Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion src/bundles/rune/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import { Channel } from '@sourceacademy/conductor/conduit';
import { DataType } from '@sourceacademy/conductor/types';
import { stringify } from 'js-slang/dist/utils/stringify';
import { describe, expect, it, test, vi } from 'vitest';
import RuneModulePlugin from '..';
import * as funcs from '../functions';
import { RUNE_CHANNEL_ID, type RuneChannelMessage } from '../protocol';
import type { Rune } from '../rune';

function makeChannelEvaluator() {
const store: unknown[] = [];
return {
hasDataInterface: true as const,
closure_make: vi.fn(async (sig, func, dependsOn) => ({
type: DataType.CLOSURE,
value: { sig, dependsOn, func }
})),
opaque_make: vi.fn(async value => {
store.push(value);
return {
type: DataType.OPAQUE,
value: store.length - 1
};
}),
opaque_get: vi.fn(async (value: { value: number }) => store[value.value])
};
}

function waitForChannelMessages() {
return new Promise(resolve => setTimeout(resolve, 50));
}

describe(RuneModulePlugin, () => {
test('exported methods stay bound when called by a Conductor closure', async () => {
const sentMessages: unknown[] = [];
let subscriber: (message: RuneChannelMessage) => void = () => {};
const channel = {
send: vi.fn(message => sentMessages.push(message)),
subscribe: vi.fn(),
subscribe: vi.fn((newSubscriber: (message: RuneChannelMessage) => void) => {
subscriber = newSubscriber;
}),
unsubscribe: vi.fn(),
close: vi.fn(),
name: 'rune-test-channel'
Expand Down Expand Up @@ -40,6 +68,7 @@ describe(RuneModulePlugin, () => {
};
const runeValue = await evaluator.opaque_make(funcs.blank);
const result = await closureObject.func.call(closureObject, runeValue).next();
subscriber({ type: 'request' });

expect(result.done).toBe(true);
expect(sentMessages).toHaveLength(1);
Expand All @@ -55,6 +84,35 @@ describe(RuneModulePlugin, () => {
});
expect('draw' in (sentMessages[0] as any).rune).toBe(false);
});

test('replays displays made before the tab request once and in order', async () => {
const { port1, port2 } = new MessageChannel();
const runnerChannel = new Channel<RuneChannelMessage>(RUNE_CHANNEL_ID, port1);
const webChannel = new Channel<RuneChannelMessage>(RUNE_CHANNEL_ID, port2);
const tabLoader = {
tabs: ['Rune'],
loadTab: vi.fn()
};
const evaluator = makeChannelEvaluator();
const plugin = new RuneModulePlugin({} as any, [runnerChannel] as any, evaluator as any, tabLoader);

await plugin.initialise();

const square = plugin.exports.find(each => each.symbol === 'square')!.value as any;
const circle = plugin.exports.find(each => each.symbol === 'circle')!.value as any;
await plugin.show(square).next();
await plugin.anaglyph(circle).next();

await waitForChannelMessages();
const received: RuneChannelMessage[] = [];
webChannel.subscribe(message => received.push(message));
webChannel.send({ type: 'request' });
await waitForChannelMessages();

const renders = received.filter(message => message.type === 'render');
expect(tabLoader.loadTab).toHaveBeenCalledExactlyOnceWith('Rune');
expect(renders.map(render => render.mode)).toEqual(['normal', 'anaglyph']);
});
});

describe('Hollusion Rune tests', () => {
Expand Down
13 changes: 7 additions & 6 deletions src/bundles/rune/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default class RuneModulePlugin extends BaseModulePlugin {
private readonly __tabLoader: RuneTabLoader | undefined;
private readonly __displayed: RuneDisplayMessage[] = [];
private __tabLoaded = false;
private __tabRequested = false;

/**
* Rune with the shape of a blank square
Expand Down Expand Up @@ -192,6 +193,7 @@ export default class RuneModulePlugin extends BaseModulePlugin {
this.__tabLoader = tabLoader;
this.__runeChannel.subscribe(message => {
if (message.type === 'request') {
this.__tabRequested = true;
this.__displayed.forEach(displayedMessage => this.__runeChannel.send(displayedMessage));
}
});
Expand All @@ -214,22 +216,21 @@ export default class RuneModulePlugin extends BaseModulePlugin {

/**
* Loads the host-side tab
* @returns Whether the tab was already loaded
*/
private __loadRuneTab(): boolean {
if (this.__tabLoaded || this.__tabLoader === undefined) return true;
private __loadRuneTab(): void {
if (this.__tabLoaded || this.__tabLoader === undefined) return;

const tabName = this.__tabLoader.tabs[0];
if (tabName === undefined) return true;
if (tabName === undefined) return;

this.__tabLoader.loadTab(tabName);
this.__tabLoaded = true;
return false;
}

private async __display(message: RuneDisplayMessage): Promise<void> {
this.__displayed.push(message);
if (this.__loadRuneTab()) {
this.__loadRuneTab();
if (this.__tabRequested) {
this.__runeChannel.send(message);
}
}
Expand Down