mirror of
https://github.com/pyscript/pyscript.git
synced 2026-03-05 14:00:04 -05:00
Refactor py-config and the general initialization logic of the page (#806)
This PR is the first step to improve and rationalize the life-cycle of a pyscript app along the lines of what I described in #763 . It is not a complete solution, more PRs will follow. Highlights: - py-config is no longer a web component: the old code relied on PyConfig.connectedCallback to do some logic, but then if no <py-config> tag was present, we had to introduce a dummy one with the sole goal of activating the callback. Now the logic is much more linear. - the new pyconfig.ts only contains the code which is needed to parse the config; I also moved some relevant code from utils.ts because it didn't really belong to it - the old PyConfig class did much more than dealing with the config: in particular, it contained the code to initialize the env and the runtime. Now this logic has been moved directly into main.ts, inside the new PyScriptApp class. I plan to refactor the initialization code in further PRs - the current code relies too much on global state and global variables, they are everywhere. This PR is a first step to solve the problem by introducing a PyScriptApp class, which will hold all the mutable state of the page. Currently only config is stored there, but eventually I will migrate more state to it, until we will have only one global singleton, globalApp - thanks to what I described above, I could kill the appConfig svelte store: one less store to kill :).
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import type { AppConfig, RuntimeConfig } from '../../src/runtime';
|
||||
import { PyConfig } from '../../src/components/pyconfig';
|
||||
import type { AppConfig, RuntimeConfig } from '../../src/pyconfig';
|
||||
import { loadConfigFromElement, defaultConfig } from '../../src/pyconfig';
|
||||
import { version } from '../../src/runtime';
|
||||
|
||||
// inspired by trump typos
|
||||
const covfefeConfig = {
|
||||
name: 'covfefe',
|
||||
@@ -11,13 +13,13 @@ const covfefeConfig = {
|
||||
lang: 'covfefe',
|
||||
},
|
||||
],
|
||||
wonerful: 'discgrace',
|
||||
wonderful: 'disgrace',
|
||||
};
|
||||
|
||||
const covfefeConfigToml = `
|
||||
name = "covfefe"
|
||||
|
||||
wonerful = "highjacked"
|
||||
wonderful = "hijacked"
|
||||
|
||||
[[runtimes]]
|
||||
src = "/demo/covfefe.js"
|
||||
@@ -25,11 +27,29 @@ name = "covfefe"
|
||||
lang = "covfefe"
|
||||
`;
|
||||
|
||||
customElements.define('py-config', PyConfig);
|
||||
|
||||
describe('PyConfig', () => {
|
||||
let instance: PyConfig;
|
||||
// ideally, I would like to be able to just do "new HTMLElement" in the tests
|
||||
// below, but it is not permitted. The easiest work around is to create a fake
|
||||
// custom element: not that we are not using any specific feature of custom
|
||||
// elements: the sole purpose to FakeElement is to be able to instantiate them
|
||||
// in the tests.
|
||||
class FakeElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
customElements.define('fake-element', FakeElement);
|
||||
|
||||
function make_config_element(attrs) {
|
||||
const el = new FakeElement();
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
el.setAttribute(key, value as string);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
describe('loadConfigFromElement', () => {
|
||||
const xhrMockClass = () => ({
|
||||
open: jest.fn(),
|
||||
send: jest.fn(),
|
||||
@@ -38,141 +58,102 @@ describe('PyConfig', () => {
|
||||
// @ts-ignore
|
||||
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass);
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new PyConfig();
|
||||
});
|
||||
|
||||
it('should get the Config to just instantiate', async () => {
|
||||
expect(instance).toBeInstanceOf(PyConfig);
|
||||
});
|
||||
|
||||
it('should load runtime from config and set as script src', () => {
|
||||
instance.values = covfefeConfig;
|
||||
instance.loadRuntimes();
|
||||
expect(document.scripts[0].src).toBe('http://localhost/demo/covfefe.js');
|
||||
});
|
||||
|
||||
it('should load the default config', () => {
|
||||
instance.connectedCallback();
|
||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
||||
// @ts-ignore
|
||||
expect(instance.values.runtimes[0].lang).toBe('python');
|
||||
const config = loadConfigFromElement(null);
|
||||
expect(config).toBe(defaultConfig);
|
||||
expect(config.pyscript.version).toBe(version);
|
||||
});
|
||||
|
||||
it('an empty <py-config> should load the default config', () => {
|
||||
const el = make_config_element({});
|
||||
let config = loadConfigFromElement(el);
|
||||
expect(config).toBe(defaultConfig);
|
||||
expect(config.pyscript.version).toBe(version);
|
||||
});
|
||||
|
||||
it('should load the JSON config from inline', () => {
|
||||
instance.setAttribute('type', 'json');
|
||||
instance.innerHTML = JSON.stringify(covfefeConfig);
|
||||
instance.connectedCallback();
|
||||
// @ts-ignore
|
||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
||||
const el = make_config_element({ type: 'json' });
|
||||
el.innerHTML = JSON.stringify(covfefeConfig);
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// schema_version wasn't present in `inline config` but is still set due to merging with default
|
||||
expect(config.schema_version).toBe(1);
|
||||
});
|
||||
|
||||
it('should load the JSON config from src attribute', () => {
|
||||
instance.setAttribute('type', 'json');
|
||||
instance.setAttribute('src', '/covfefe.json');
|
||||
instance.connectedCallback();
|
||||
// @ts-ignore
|
||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
||||
// wonerful is an extra key supplied by the user and is unaffected by merging process
|
||||
expect(instance.values.wonerful).toBe('discgrace');
|
||||
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// wonderful is an extra key supplied by the user and is unaffected by merging process
|
||||
expect(config.wonderful).toBe('disgrace');
|
||||
// schema_version wasn't present in `config from src` but is still set due to merging with default
|
||||
expect(config.schema_version).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('should load the JSON config from both inline and src', () => {
|
||||
instance.setAttribute('type', 'json');
|
||||
instance.innerHTML = JSON.stringify({ version: '0.2a', wonerful: 'highjacked' });
|
||||
instance.setAttribute('src', '/covfefe.json');
|
||||
instance.connectedCallback();
|
||||
// @ts-ignore
|
||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
||||
// config from src had an extra key "wonerful" with value "discgrace"
|
||||
// inline config had the same extra key "wonerful" with value "highjacked"
|
||||
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||
el.innerHTML = JSON.stringify({ version: '0.2a', wonderful: 'hijacked' });
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// config from src had an extra key "wonderful" with value "disgrace"
|
||||
// inline config had the same extra key "wonderful" with value "hijacked"
|
||||
// the merge process works for extra keys that clash as well
|
||||
// so the final value is "highjacked" since inline takes precedence over src
|
||||
expect(instance.values.wonerful).toBe('highjacked');
|
||||
// so the final value is "hijacked" since inline takes precedence over src
|
||||
expect(config.wonderful).toBe('hijacked');
|
||||
// version wasn't present in `config from src` but is still set due to merging with default and inline
|
||||
expect(instance.values.version).toBe('0.2a');
|
||||
expect(config.version).toBe('0.2a');
|
||||
});
|
||||
|
||||
it('should be able to load an inline TOML config', () => {
|
||||
// type of config is TOML if not supplied
|
||||
instance.innerHTML = covfefeConfigToml;
|
||||
instance.connectedCallback();
|
||||
// @ts-ignore
|
||||
expect(instance.values.runtimes[0].lang).toBe('covfefe');
|
||||
expect(instance.values.pyscript?.time).not.toBeNull();
|
||||
expect(instance.values.wonerful).toBe('highjacked');
|
||||
// TOML is the default type
|
||||
const el = make_config_element({});
|
||||
el.innerHTML = covfefeConfigToml;
|
||||
const config = loadConfigFromElement(el);
|
||||
expect(config.runtimes[0].lang).toBe('covfefe');
|
||||
expect(config.pyscript?.time).not.toBeNull();
|
||||
// schema_version wasn't present in `inline config` but is still set due to merging with default
|
||||
expect(config.schema_version).toBe(1);
|
||||
expect(config.wonderful).toBe('hijacked');
|
||||
});
|
||||
|
||||
it.failing('should NOT be able to load an inline config in JSON format with type as TOML', () => {
|
||||
instance.innerHTML = JSON.stringify(covfefeConfig);
|
||||
instance.connectedCallback();
|
||||
it('should NOT be able to load an inline config in JSON format with type as TOML', () => {
|
||||
const el = make_config_element({});
|
||||
el.innerHTML = JSON.stringify(covfefeConfig);
|
||||
expect(()=>loadConfigFromElement(el)).toThrow(/config supplied: {.*} is an invalid TOML and cannot be parsed/);
|
||||
});
|
||||
|
||||
it.failing('should NOT be able to load an inline config in TOML format with type as JSON', () => {
|
||||
instance.setAttribute('type', 'json');
|
||||
instance.innerHTML = covfefeConfigToml;
|
||||
instance.connectedCallback();
|
||||
it('should NOT be able to load an inline config in TOML format with type as JSON', () => {
|
||||
const el = make_config_element({ type: 'json' });
|
||||
el.innerHTML = covfefeConfigToml;
|
||||
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as toml', () => {
|
||||
instance.innerHTML = covfefeConfigToml;
|
||||
instance.setAttribute('src', '/covfefe.json');
|
||||
instance.connectedCallback();
|
||||
it('should NOT be able to load an inline TOML config with a JSON config from src with type as toml', () => {
|
||||
const el = make_config_element({ src: '/covfefe.json' });
|
||||
el.innerHTML = covfefeConfigToml;
|
||||
expect(()=>loadConfigFromElement(el)).toThrow(/config supplied: {.*} is an invalid TOML and cannot be parsed/);
|
||||
});
|
||||
|
||||
it.failing('should NOT be able to load an inline TOML config with a JSON config from src with type as json', () => {
|
||||
instance.setAttribute('type', 'json');
|
||||
instance.innerHTML = covfefeConfigToml;
|
||||
instance.setAttribute('src', '/covfefe.json');
|
||||
instance.connectedCallback();
|
||||
it('should NOT be able to load an inline TOML config with a JSON config from src with type as json', () => {
|
||||
const el = make_config_element({ type: 'json', src: '/covfefe.json' });
|
||||
el.innerHTML = covfefeConfigToml;
|
||||
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('should error out when passing an invalid JSON', () => {
|
||||
instance.setAttribute('type', 'json');
|
||||
instance.innerHTML = '[[';
|
||||
expect(()=>instance.connectedCallback()).toThrow(SyntaxError);
|
||||
const el = make_config_element({ type: 'json' });
|
||||
el.innerHTML = '[[';
|
||||
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('should error out when passing an invalid TOML', () => {
|
||||
instance.innerHTML = '[[';
|
||||
expect(()=>instance.connectedCallback()).toThrow(SyntaxError);
|
||||
const el = make_config_element({});
|
||||
el.innerHTML = '[[';
|
||||
expect(()=>loadConfigFromElement(el)).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('connectedCallback should call loadRuntimes', async () => {
|
||||
const mockedMethod = jest.fn();
|
||||
instance.loadRuntimes = mockedMethod;
|
||||
|
||||
instance.connectedCallback();
|
||||
|
||||
expect(mockedMethod).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('confirm connectedCallback happy path', async () => {
|
||||
const mockedMethod = jest.fn();
|
||||
instance.loadRuntimes = mockedMethod;
|
||||
instance.innerHTML = 'test = 42';
|
||||
|
||||
instance.connectedCallback();
|
||||
|
||||
expect(instance.code).toBe('test = 42');
|
||||
expect(instance.values['test']).toBe(42);
|
||||
});
|
||||
|
||||
it('log should add new message to the page', async () => {
|
||||
// details are undefined, so let's create a div for it
|
||||
instance.details = document.createElement('div');
|
||||
instance.log('this is a log');
|
||||
|
||||
// @ts-ignore: typescript complains about accessing innerText
|
||||
expect(instance.details.childNodes[0].innerText).toBe('this is a log');
|
||||
});
|
||||
|
||||
it('confirm that calling close would call this.remove', async () => {
|
||||
instance.remove = jest.fn();
|
||||
instance.close();
|
||||
expect(instance.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AppConfig } from '../../src/pyconfig';
|
||||
import { Runtime } from '../../src/runtime';
|
||||
import { PyodideRuntime } from '../../src/pyodide';
|
||||
|
||||
@@ -8,7 +9,8 @@ global.TextDecoder = TextDecoder
|
||||
describe('PyodideRuntime', () => {
|
||||
let runtime: PyodideRuntime;
|
||||
beforeAll(async () => {
|
||||
runtime = new PyodideRuntime();
|
||||
const config: AppConfig = {};
|
||||
runtime = new PyodideRuntime(config);
|
||||
/**
|
||||
* Since import { loadPyodide } from 'pyodide';
|
||||
* is not used inside `src/pyodide.ts`, the function
|
||||
|
||||
Reference in New Issue
Block a user