squiffy-runtime 6.0.0-alpha.1 → 6.0.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`"Hello world" script should run 1`] = `
4
+ "
5
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Hello world</p></div></div></div>"
6
+ `;
7
+
8
+ exports[`Click a passage link 1`] = `
9
+ "
10
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="Introduction"><div class="squiffy-output-block"><div data-source="[[Introduction]]"><h1>Squiffy Test</h1>
11
+ <p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
12
+ <p>This should be <a class="squiffy-link link-passage" data-passage="passage" role="link" tabindex="0">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
13
+ <p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
14
+ <p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
15
+ <p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
16
+ <p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
17
+ <p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div></div></div>"
18
+ `;
19
+
20
+ exports[`Click a passage link 2`] = `
21
+ "
22
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="Introduction"><div class="squiffy-output-block"><div data-source="[[Introduction]]"><h1>Squiffy Test</h1>
23
+ <p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
24
+ <p>This should be <a class="squiffy-link link-passage disabled" data-passage="passage" role="link" tabindex="-1">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
25
+ <p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
26
+ <p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
27
+ <p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
28
+ <p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
29
+ <p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div></div><div class="squiffy-output-block"><div data-source="[[Introduction]][passage]"><p>Here's some text for the passage.</p></div></div></div>"
30
+ `;
31
+
32
+ exports[`Click a section link 1`] = `
33
+ "
34
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="Introduction"><div class="squiffy-output-block"><div data-source="[[Introduction]]"><h1>Squiffy Test</h1>
35
+ <p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
36
+ <p>This should be <a class="squiffy-link link-passage" data-passage="passage" role="link" tabindex="0">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
37
+ <p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
38
+ <p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
39
+ <p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
40
+ <p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
41
+ <p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div></div></div>"
42
+ `;
43
+
44
+ exports[`Click a section link 2`] = `
45
+ "
46
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="Introduction"><div class="squiffy-output-block"><div data-source="[[Introduction]]"><h1>Squiffy Test</h1>
47
+ <p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
48
+ <p>This should be <a class="squiffy-link link-passage" data-passage="passage" role="link" tabindex="0">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
49
+ <p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
50
+ <p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
51
+ <p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
52
+ <p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
53
+ <p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div></div></div><div class="squiffy-output-section" id="squiffy-section-2" data-section="section 3"><div class="squiffy-output-block"><div data-source="[[section 3]]"><p>Another section is here, with passages <a class="squiffy-link link-passage" data-passage="a" role="link" tabindex="0">a</a> and <a class="squiffy-link link-passage" data-passage="b" role="link" tabindex="0">b</a>.</p></div></div></div>"
54
+ `;
55
+
56
+ exports[`Delete passage 1`] = `
57
+ "
58
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a]"><p>New passage</p></div></div></div>"
59
+ `;
60
+
61
+ exports[`Delete passage 2`] = `
62
+ "
63
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div></div>"
64
+ `;
65
+
66
+ exports[`Delete section 1`] = `
67
+ "
68
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-section" data-section="a" role="link" tabindex="0">a</a></p></div></div></div><div class="squiffy-output-section" id="squiffy-section-2" data-section="a"><div class="squiffy-output-block"><div data-source="[[a]]"><p>New section</p></div></div></div>"
69
+ `;
70
+
71
+ exports[`Delete section 2`] = `
72
+ "
73
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-section" data-section="a" role="link" tabindex="0">a</a></p></div></div></div>"
74
+ `;
75
+
76
+ exports[`Update default section output 1`] = `
77
+ "
78
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Hello world</p></div></div></div>"
79
+ `;
80
+
81
+ exports[`Update default section output 2`] = `
82
+ "
83
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Updated content</p></div></div></div>"
84
+ `;
85
+
86
+ exports[`Update passage output - passage name "a" 1`] = `
87
+ "
88
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a]"><p>Passage a content</p></div></div></div>"
89
+ `;
90
+
91
+ exports[`Update passage output - passage name "a" 2`] = `
92
+ "
93
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a]"><p>Updated passage content</p></div></div></div>"
94
+ `;
95
+
96
+ exports[`Update passage output - passage name "a'1" 1`] = `
97
+ "
98
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a'1" role="link" tabindex="-1">a'1</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a'1]"><p>Passage a content</p></div></div></div>"
99
+ `;
100
+
101
+ exports[`Update passage output - passage name "a'1" 2`] = `
102
+ "
103
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a'1" role="link" tabindex="-1">a'1</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a'1]"><p>Updated passage content</p></div></div></div>"
104
+ `;
package/src/events.ts ADDED
@@ -0,0 +1,42 @@
1
+ type LinkType = 'section' | 'passage' | 'rotate' | 'sequence';
2
+
3
+ export type SquiffyEventMap = {
4
+ linkClick: { linkType: LinkType }; // a story link was clicked
5
+ };
6
+
7
+ export type SquiffyEventHandler<E extends keyof SquiffyEventMap> =
8
+ (payload: SquiffyEventMap[E]) => void;
9
+
10
+ export class Emitter<Events extends Record<string, any>> {
11
+ private listeners = new Map<keyof Events, Set<Function>>();
12
+
13
+ on<E extends keyof Events>(event: E, handler: (p: Events[E]) => void) {
14
+ if (!this.listeners.has(event)) this.listeners.set(event, new Set());
15
+ this.listeners.get(event)!.add(handler);
16
+ return () => this.off(event, handler);
17
+ }
18
+
19
+ off<E extends keyof Events>(event: E, handler: (p: Events[E]) => void) {
20
+ this.listeners.get(event)?.delete(handler);
21
+ }
22
+
23
+ once<E extends keyof Events>(event: E, handler: (p: Events[E]) => void) {
24
+ const off = this.on(event, (payload) => {
25
+ off();
26
+ handler(payload);
27
+ });
28
+ return off;
29
+ }
30
+
31
+ emit<E extends keyof Events>(event: E, payload: Events[E]) {
32
+ // Fire handlers asynchronously so the runtime isn't blocked by user code.
33
+ queueMicrotask(() => {
34
+ this.listeners.get(event)?.forEach(h => {
35
+ try { (h as any)(payload); } catch (err) {
36
+ // Swallow so a bad handler doesn't break the game; optionally log.
37
+ console.error(`[Squiffy] handler for "${String(event)}" failed`, err);
38
+ }
39
+ });
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,413 @@
1
+ import { expect, test, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs/promises';
3
+ import globalJsdom from 'global-jsdom';
4
+ import { init } from './squiffy.runtime.js';
5
+ import { compile as squiffyCompile } from 'squiffy-compiler';
6
+
7
+ const html = `
8
+ <!DOCTYPE html>
9
+ <html>
10
+ <head>
11
+ </head>
12
+ <body>
13
+ <div id="squiffy">
14
+ </div>
15
+ <div id="test">
16
+ </div>
17
+ </body>
18
+ </html>
19
+ `;
20
+
21
+ const compile = async (script: string) => {
22
+ const compileResult = await squiffyCompile({
23
+ script: script,
24
+ });
25
+
26
+ if (!compileResult.success) {
27
+ throw new Error('Compile failed');
28
+ }
29
+
30
+ const story = compileResult.output.story;
31
+ const js = compileResult.output.js.map(jsLines => new Function('squiffy', 'get', 'set', jsLines.join('\n')));
32
+
33
+ return {
34
+ story: {
35
+ js: js as any,
36
+ ...story,
37
+ },
38
+ };
39
+ }
40
+
41
+ const initScript = async (script: string) => {
42
+ const element = document.getElementById('squiffy');
43
+
44
+ if (!element) {
45
+ throw new Error('Element not found');
46
+ }
47
+
48
+ const compileResult = await compile(script);
49
+
50
+ const squiffyApi = init({
51
+ element: element,
52
+ story: compileResult.story,
53
+ });
54
+
55
+ return {
56
+ squiffyApi,
57
+ element
58
+ }
59
+ };
60
+
61
+ const findLink = (element: HTMLElement, linkType: string, linkText: string, onlyEnabled: boolean = false) => {
62
+ const links = onlyEnabled
63
+ ? element.querySelectorAll(`.squiffy-output-section:last-child a.squiffy-link.link-${linkType}`)
64
+ : element.querySelectorAll(`a.squiffy-link.link-${linkType}`);
65
+ return Array.from(links).find(link => link.textContent === linkText && (onlyEnabled ? !link.classList.contains("disabled") : true)) as HTMLElement;
66
+ };
67
+
68
+ const getTestOutput = () => {
69
+ const testElement = document.getElementById('test');
70
+ if (!testElement) {
71
+ throw new Error('Test element not found');
72
+ }
73
+ return testElement.innerText;
74
+ }
75
+
76
+ let cleanup: { (): void };
77
+
78
+ beforeEach(() => {
79
+ cleanup = globalJsdom(html);
80
+ });
81
+
82
+ afterEach(() => {
83
+ cleanup();
84
+ });
85
+
86
+ test('"Hello world" script should run', async () => {
87
+ const { element } = await initScript("Hello world");
88
+ expect(element.innerHTML).toMatchSnapshot();
89
+ });
90
+
91
+ test('Click a section link', async () => {
92
+ const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
93
+ const { squiffyApi, element } = await initScript(script);
94
+ expect(element.innerHTML).toMatchSnapshot();
95
+
96
+ expect(element.querySelectorAll('a.squiffy-link').length).toBe(10);
97
+ const linkToPassage = findLink(element, 'passage', 'a link to a passage');
98
+ const section3Link = findLink(element, 'section', 'section 3');
99
+
100
+ expect(linkToPassage).not.toBeNull();
101
+ expect(section3Link).not.toBeNull();
102
+ expect(linkToPassage.classList).not.toContain('disabled');
103
+
104
+ const handler = vi.fn();
105
+ const off = squiffyApi.on('linkClick', handler);
106
+
107
+ const handled = squiffyApi.clickLink(section3Link);
108
+ expect(handled).toBe(true);
109
+
110
+ expect(element.innerHTML).toMatchSnapshot();
111
+
112
+ // passage link is from the previous section, so should be unclickable
113
+ expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
114
+
115
+ // await for the event to be processed
116
+ await Promise.resolve();
117
+
118
+ expect(handler).toHaveBeenCalledTimes(1);
119
+ expect(handler).toHaveBeenCalledWith(
120
+ expect.objectContaining({ linkType: 'section' })
121
+ );
122
+ off();
123
+ });
124
+
125
+ test('Click a passage link', async () => {
126
+ const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
127
+ const { squiffyApi, element } = await initScript(script);
128
+ expect(element.innerHTML).toMatchSnapshot();
129
+
130
+ expect(element.querySelectorAll('a.squiffy-link').length).toBe(10);
131
+ const linkToPassage = findLink(element, 'passage', 'a link to a passage');
132
+ const section3Link = findLink(element, 'section', 'section 3');
133
+
134
+ expect(linkToPassage).not.toBeNull();
135
+ expect(section3Link).not.toBeNull();
136
+ expect(linkToPassage.classList).not.toContain('disabled');
137
+
138
+ const handler = vi.fn();
139
+ const off = squiffyApi.on('linkClick', handler);
140
+
141
+ const handled = squiffyApi.clickLink(linkToPassage);
142
+ expect(handled).toBe(true);
143
+
144
+ expect(linkToPassage.classList).toContain('disabled');
145
+ expect(element.innerHTML).toMatchSnapshot();
146
+
147
+ // shouldn't be able to click it again
148
+ expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
149
+
150
+ // await for the event to be processed
151
+ await Promise.resolve();
152
+
153
+ expect(handler).toHaveBeenCalledTimes(1);
154
+ expect(handler).toHaveBeenCalledWith(
155
+ expect.objectContaining({ linkType: 'passage' })
156
+ );
157
+ off();
158
+ });
159
+
160
+ test('Run JavaScript functions', async () => {
161
+ const script = `
162
+ document.getElementById('test').innerText = 'Initial JavaScript';
163
+ @set some_string = some_value
164
+ @set some_number = 5
165
+
166
+ +++Continue...
167
+ document.getElementById('test').innerText = "Value: " + get("some_number");
168
+ +++Continue...
169
+ document.getElementById('test').innerText = "Value: " + get("some_string");
170
+ set("some_number", 10);
171
+ +++Continue...
172
+ document.getElementById('test').innerText = "Value: " + get("some_number");
173
+ +++Continue...
174
+ @inc some_number
175
+ +++Continue...
176
+ document.getElementById('test').innerText = "Value: " + get("some_number");
177
+ +++Continue...
178
+ squiffy.story.go("other section");
179
+ [[other section]]:
180
+ document.getElementById('test').innerText = "In other section";
181
+ `;
182
+
183
+ const clickContinue = () => {
184
+ const continueLink = findLink(element, 'section', 'Continue...', true);
185
+ expect(continueLink).not.toBeNull();
186
+ const handled = squiffyApi.clickLink(continueLink);
187
+ expect(handled).toBe(true);
188
+ };
189
+
190
+ const { squiffyApi, element } = await initScript(script);
191
+
192
+ expect(getTestOutput()).toBe('Initial JavaScript');
193
+ clickContinue();
194
+
195
+ expect(getTestOutput()).toBe('Value: 5');
196
+ clickContinue();
197
+
198
+ expect(getTestOutput()).toBe('Value: some_value');
199
+ clickContinue();
200
+
201
+ expect(getTestOutput()).toBe('Value: 10');
202
+
203
+ clickContinue();
204
+ clickContinue();
205
+ expect(getTestOutput()).toBe('Value: 11');
206
+
207
+ clickContinue();
208
+ expect(getTestOutput()).toBe('In other section');
209
+ });
210
+
211
+ function safeQuerySelector(name: string) {
212
+ return name.replace(/'/g, "\\'");
213
+ }
214
+
215
+ function getSectionContent(element: HTMLElement, section: string) {
216
+ return element.querySelector(`[data-source='[[${safeQuerySelector(section)}]]'] p`)?.textContent || null;
217
+ }
218
+
219
+ function getPassageContent(element: HTMLElement, section: string, passage: string) {
220
+ return element.querySelector(`[data-source='[[${safeQuerySelector(section)}]][${safeQuerySelector(passage)}]'] p`)?.textContent || null;
221
+ }
222
+
223
+ test('Update default section output', async () => {
224
+ const { squiffyApi, element } = await initScript("Hello world");
225
+ let defaultOutput = getSectionContent(element, '_default');
226
+ expect(defaultOutput).toBe('Hello world');
227
+ expect(element.innerHTML).toMatchSnapshot();
228
+
229
+ const updated = await compile("Updated content");
230
+ squiffyApi.update(updated.story);
231
+ defaultOutput = getSectionContent(element, '_default');
232
+ expect(defaultOutput).toBe('Updated content');
233
+ expect(element.innerHTML).toMatchSnapshot();
234
+ });
235
+
236
+ test.each(['a', 'a\'1'])('Update passage output - passage name "%s"', async (name) => {
237
+ const { squiffyApi, element } = await initScript(`Click this: [${name}]
238
+
239
+ [${name}]:
240
+ Passage a content`);
241
+
242
+ const link = findLink(element, 'passage', name);
243
+ const handled = squiffyApi.clickLink(link);
244
+ expect(handled).toBe(true);
245
+
246
+ let defaultOutput = getSectionContent(element, '_default');
247
+ expect(defaultOutput).toBe(`Click this: ${name}`);
248
+ let passageOutput = getPassageContent(element, '_default', name);
249
+ expect(passageOutput).toBe('Passage a content');
250
+ expect(element.innerHTML).toMatchSnapshot();
251
+
252
+ const updated = await compile(`Click this: [${name}]
253
+
254
+ [${name}]:
255
+ Updated passage content`);
256
+
257
+ squiffyApi.update(updated.story);
258
+
259
+ defaultOutput = getSectionContent(element, '_default');
260
+ expect(defaultOutput).toBe(`Click this: ${name}`);
261
+
262
+ passageOutput = getPassageContent(element, '_default', name);
263
+ expect(passageOutput).toBe('Updated passage content');
264
+ expect(element.innerHTML).toMatchSnapshot();
265
+ });
266
+
267
+ test('Delete section', async () => {
268
+ const { squiffyApi, element } = await initScript(`Click this: [[a]]
269
+
270
+ [[a]]:
271
+ New section`);
272
+
273
+ const link = findLink(element, 'section', 'a');
274
+ const handled = squiffyApi.clickLink(link);
275
+ expect(handled).toBe(true);
276
+
277
+ let defaultOutput = getSectionContent(element, '_default');
278
+ expect(defaultOutput).toBe('Click this: a');
279
+ let sectionOutput = getSectionContent(element, 'a');
280
+ expect(sectionOutput).toBe('New section');
281
+ expect(element.innerHTML).toMatchSnapshot();
282
+
283
+ const updated = await compile(`Click this: [[a]]`);
284
+
285
+ squiffyApi.update(updated.story);
286
+
287
+ defaultOutput = getSectionContent(element, '_default');
288
+ expect(defaultOutput).toBe('Click this: a');
289
+ sectionOutput = getSectionContent(element, 'a');
290
+ expect(sectionOutput).toBeNull();
291
+
292
+ expect(element.innerHTML).toMatchSnapshot();
293
+ });
294
+
295
+ test('Delete passage', async () => {
296
+ const { squiffyApi, element } = await initScript(`Click this: [a]
297
+
298
+ [a]:
299
+ New passage`);
300
+
301
+ const link = findLink(element, 'passage', 'a');
302
+ const handled = squiffyApi.clickLink(link);
303
+ expect(handled).toBe(true);
304
+
305
+ let defaultOutput = getSectionContent(element, '_default');
306
+ expect(defaultOutput).toBe('Click this: a');
307
+ let passageOutput = getPassageContent(element, '_default', 'a');
308
+ expect(passageOutput).toBe('New passage');
309
+ expect(element.innerHTML).toMatchSnapshot();
310
+
311
+ const updated = await compile(`Click this: [a]`);
312
+
313
+ squiffyApi.update(updated.story);
314
+
315
+ defaultOutput = getSectionContent(element, '_default');
316
+ expect(defaultOutput).toBe('Click this: a');
317
+ passageOutput = getPassageContent(element, '_default', 'a');
318
+ expect(passageOutput).toBeNull();
319
+
320
+ expect(element.innerHTML).toMatchSnapshot();
321
+ });
322
+
323
+ test('Clicked passage links remain disabled after an update', async () => {
324
+ const { squiffyApi, element } = await initScript(`Click one of these: [a] [b]
325
+
326
+ [a]:
327
+ Output for passage A.
328
+
329
+ [b]:
330
+ Output for passage B.`);
331
+
332
+ // click linkA
333
+
334
+ let linkA = findLink(element, 'passage', 'a');
335
+ expect(linkA.classList).not.toContain('disabled');
336
+ expect(squiffyApi.clickLink(linkA)).toBe(true);
337
+
338
+ const updated = await compile(`Click one of these (updated): [a] [b]
339
+
340
+ [a]:
341
+ Output for passage A.
342
+
343
+ [b]:
344
+ Output for passage B.`);
345
+
346
+ squiffyApi.update(updated.story);
347
+
348
+ // linkA should still be disabled
349
+
350
+ linkA = findLink(element, 'passage', 'a');
351
+ expect(linkA.classList).toContain('disabled');
352
+ expect(squiffyApi.clickLink(linkA)).toBe(false);
353
+
354
+ // linkB should still be enabled
355
+
356
+ let linkB = findLink(element, 'passage', 'b');
357
+ expect(linkB.classList).not.toContain('disabled');
358
+ expect(squiffyApi.clickLink(linkB)).toBe(true);
359
+ });
360
+
361
+ test('Deleting the current section activates the previous section', async () => {
362
+ const { squiffyApi, element } = await initScript(`Choose a section: [[a]] [[b]], or passage [start1].
363
+
364
+ [start1]:
365
+ Output for passage start1.
366
+
367
+ [[a]]:
368
+ Output for section A.
369
+
370
+ [[b]]:
371
+ Output for section B.`);
372
+
373
+ // click linkA
374
+
375
+ let linkA = findLink(element, 'section', 'a');
376
+ let linkB = findLink(element, 'section', 'b');
377
+ expect(linkA.classList).not.toContain('disabled');
378
+ expect(squiffyApi.clickLink(linkA)).toBe(true);
379
+
380
+ // can't click start1 passage as we're in section [[a]] now
381
+ let linkStart1 = findLink(element, 'passage', 'start1');
382
+ expect(squiffyApi.clickLink(linkStart1)).toBe(false);
383
+
384
+ // can't click linkB as we're in section [[a]] now
385
+ expect(squiffyApi.clickLink(linkB)).toBe(false);
386
+
387
+ // now we delete section [[a]]
388
+
389
+ const updated = await compile(`Choose a section: [[a]] [[b]], or passage [start1].
390
+
391
+ [start1]:
392
+ Output for passage start1.
393
+
394
+ [[b]]:
395
+ Output for section B. Here's a passage: [b1].
396
+
397
+ [b1]:
398
+ Passage in section B.`);
399
+
400
+ squiffyApi.update(updated.story);
401
+
402
+ // We're in the first section, so the start1 passage should be clickable now
403
+ linkStart1 = findLink(element, 'passage', 'start1');
404
+ expect(squiffyApi.clickLink(linkStart1)).toBe(true);
405
+
406
+ // We're in the first section, so linkB should be clickable now
407
+ linkB = findLink(element, 'section', 'b');
408
+ expect(squiffyApi.clickLink(linkB)).toBe(true);
409
+
410
+ // and the passage [b1] within it should be clickable
411
+ const linkB1 = findLink(element, 'passage', 'b1');
412
+ expect(squiffyApi.clickLink(linkB1)).toBe(true);
413
+ });