Snowman Dev Update (2.2: 14 June 2022)

Snowman 2.2 Development Updates:


I usually create these posts on weekends based on notes I write for myself on what was done throughout the week. For this post, however, I thought I would document something I’ve been doing as a separate post outside of the weekly cycle I’ve been following.

Issues for automating story format unit tests

A Twine 2 story format is a complicated thing. While there is a specification explaining most of its parts, the principle parts for this post are the following:

  • JavaScript code for handling how a reader interacts with a story
  • HTML source code containing the JavaScript code
  • A file (traditionally named format.js) containing JSON-like properties for things like name, description, and for its source property, the combined source of the HTML file with the JavaScript code embedded.

These three parts complicate the testing of a story format. Normally, for most web development projects, the code being tested is some combination of JavaScript, CSS, and HTML. A webpage can be created with the new code and unit tests written against it to verify its user interface and internal functionality. As it comes to a story format, this is not possible because of how Twine handles story formats.

When a story is played or published with Twine 2, the story format is combined with the HTML output created from the authored passages. In order for the story format code to run, there must first exist some authored passages. Once these exist, they can be compiled together with the story format and the HTML output tested. However, at least as publicly documented, there is no way to automate the binary executable version of Twine. To test a story format, one possible approach would be for an author to create code to automate multiple processes using the online version of Twine 2 of adding a new story format, editing a story, and then using the publish functionality before then proceeding with the unit tests. Automating all these processes would be quite cumbersome.

Instead of using Twine directly, a Twee compiler can be used to create Twine-compatible projects. I happen to have my own named Extwee, although others exist. Using a twee compiler, a plain-text version of passages (Twee code) can be created and then compiled with the story format into a HTML file. Then, the created HTML file can be tested using any specific unit tests.

Combining Jest + Puppeteer + Extwee

For my set of new unit tests, I use three tools in conjunction with other. The first is Jest, a testing framework. My other unit tests for Snowman already use Jest, so any automated testing I would add would have to also be something Jest could intake and process in some way. The second tool is Puppeteer. This is one of a number of different headless browser testing frameworks currently under active development by different teams. Puppeteer was chosen over others like WebDriverIO or Selenium’s WebDriver because there exists a plugin for using Puppeteer with Jest.

In my new code, the testing process starts with a Jest describe block for the functionality I am testing and then, using Puppeteer, a local HTML file is loaded and unit tests performed. However, in order to generate the HTML file, I need one last tool: Extwee. In the new process, the testing code is expressed as twee. When the test is run, the twee code is compiled with the latest build of the story format format.js file into a HTML file.

While I think some parts of the new process could be much more generalized in the future, one of the first tests I wrote using this new process creates a HTML file using the twee code from the “Adding Functionality” example from the Twine Cookbook. It looks like the following:

/**
 * @jest-environment puppeteer
 */
const ShellJS = require('shelljs');
const path = require('path');
require('expect-puppeteer');

// Create the index.html file to test
ShellJS.exec("extwee -c -s dist/snowman-2.2.0-format.js -i test/Cookbook/AddingFunctionality/snowman_adding_functionality.twee -o test/Cookbook/AddingFunctionality/index.html");

describe('Cookbook - Adding Functionality', () => {
  beforeAll(async () => {
    await page.goto(`file://${path.join(__dirname, 'index.html')}`);
  });

  it('Should display current year on page', async () => {
    const year = new Date().getFullYear().toString();
    await expect(page).toMatch(year);
  });
});

In the above code, ShellJS is used to run Extwee, which generates the new HTML file based on the twee code. This is then loaded by Puppeteer and a unit test run.

The twee code, from the example in the Twine Cookbook, is the following:

:: StoryTitle
Adding Functionality in Snowman

:: UserScript[script]
// Use or create window.setup
window.setup = window.setup || {};

// Create global function
window.setup.showCurrentTime = function() {
	return new Date();
}

:: Start
The current time is <%= setup.showCurrentTime() %>

When the above code is run in Snowman, it should execute the code in the passage with the ‘script’ tag first. When the story starts, the starting passage (which defaults to a passage with the name of ‘Start) is processed and any template code run. In this particular example, the global function window.setup.showCurrentTime() will be executed and its output replaced inside the template tags. Effectively, if a reader were to run the story, they would see “The current time is” along with the full month, day, and current time including time zone reported by the computer:

Screenshot of “Adding Functionality” Snowman example from Twine Cookbook showing current time before screenshot was taken

In the Jest test, the specialized method toMatch() is used to test for the string value of the current year in the resulting page. If this appears, the unit text passed because the story format correctly processed (A) the new JavaScript global function before the story started, (B) the starting passage template code, and (C) the replacement of the global function with text including the current year.

Insuring backward compatibility with Twine Cookbook examples

Creating the testing block for the “Adding Functionality” example from the Twine Cookbook was the first of a much longer process of insuring Snowman 2.2 works with most if not all existing Snowman code in the Twine Cookbook. Two years ago, when I was last working on Snowman, I checked most of the Twine Cookbook examples by hand through creating new builds and reviewing them. This new process allows me to write tests for all twee code examples and what their output should show or operate like a potential reader. However, it is also the first of over 40+ code examples in the Twine Cookbook I will now need to account for to insure the best backward compatibility for Snowman 2.2. While automating the tests will allow me to test future builds faster, there is some substantial work in front of me to create another 40+ tests for Snowman in the coming weeks.