mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
reload app_window on every code execution
This commit is contained in:
39
README.md
39
README.md
@@ -78,10 +78,6 @@ If a module path is a non-local path including a protocol and a host then it is
|
|||||||
|
|
||||||
Now the module is imported as a black box - you cannot debug `BigNumber` methods.
|
Now the module is imported as a black box - you cannot debug `BigNumber` methods.
|
||||||
|
|
||||||
Currently every external is loaded once and cached until Leporello is restarted
|
|
||||||
(TODO change path to modules every time it changed on disk, since modules are
|
|
||||||
served from service workers).
|
|
||||||
|
|
||||||
## IO
|
## IO
|
||||||
|
|
||||||
To enhance the interactive experience, Leporello.js traces the calls made to IO functions within your application. This trace can be replayed later, enabling you to program iteratively by making incremental changes to your code and promptly receiving feedback.
|
To enhance the interactive experience, Leporello.js traces the calls made to IO functions within your application. This trace can be replayed later, enabling you to program iteratively by making incremental changes to your code and promptly receiving feedback.
|
||||||
@@ -197,6 +193,41 @@ allows to share localStorage between host Leporello.js instance and window
|
|||||||
where code is run)
|
where code is run)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## Saving state between page reloads
|
||||||
|
|
||||||
|
Leporello.js allows preserving the state of the application between page reloads. To achieve this, Leporello.js provides a special API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
window.leporello.storage.get(key: string)
|
||||||
|
window.leporello.storage.set(key: string, value: any)
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike localStorage and sessionStorage, these functions allow saving and retrieving non-serializable objects.
|
||||||
|
|
||||||
|
The storage can be cleared using the "(Re)open app window" button.
|
||||||
|
|
||||||
|
You can try the online demo [here](https://app.leporello.tech/?example=todos-preact). Create TODO items, then edit the code, and you will observe that your TODOs are preserved.
|
||||||
|
|
||||||
|
The code for interacting with the Leporello API is in the file `app.js`. When `app.js` module initializes, it checks whether Leporello.js API is present and loads app state:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let state
|
||||||
|
|
||||||
|
if(globalThis.leporello) {
|
||||||
|
// Get initial state from Leporello storage
|
||||||
|
state = globalThis.leporello.storage.get('state')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Later, when state changes, it saves it back to the storage:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// on state change
|
||||||
|
if(globalThis.leporello) {
|
||||||
|
// Save state to Leporello storage to load it after page reload
|
||||||
|
globalThis.leporello.storage.set('state', state)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Run Leporello locally
|
## Run Leporello locally
|
||||||
To run it locally, you need to clone repo to local folder and serve it via HTTPS protocol (HTTPS is required by [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)). See [How to use HTTPS for local development](https://web.dev/how-to-use-local-https/)
|
To run it locally, you need to clone repo to local folder and serve it via HTTPS protocol (HTTPS is required by [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)). See [How to use HTTPS for local development](https://web.dev/how-to-use-local-https/)
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ import {render} from 'https://unpkg.com/preact?module';
|
|||||||
|
|
||||||
let state, component, root
|
let state, component, root
|
||||||
|
|
||||||
|
if(globalThis.leporello) {
|
||||||
|
// See https://github.com/leporello-js/leporello-js?tab=readme-ov-file#saving-state-between-page-reloads
|
||||||
|
// Get initial state from Leporello storage
|
||||||
|
state = globalThis.leporello.storage.get('state')
|
||||||
|
}
|
||||||
|
|
||||||
export const createApp = initial => {
|
export const createApp = initial => {
|
||||||
/* if state is already initialized then preserve it */
|
/* if state was loaded from Leporello storage then keep it,
|
||||||
|
* otherwise initialize with initial state */
|
||||||
state = state ?? initial.initialState
|
state = state ?? initial.initialState
|
||||||
component = initial.component
|
component = initial.component
|
||||||
root = initial.root
|
root = initial.root
|
||||||
@@ -12,6 +19,10 @@ export const createApp = initial => {
|
|||||||
|
|
||||||
export const handler = fn => (...args) => {
|
export const handler = fn => (...args) => {
|
||||||
state = fn(state, ...args)
|
state = fn(state, ...args)
|
||||||
|
if(globalThis.leporello) {
|
||||||
|
// Save state to Leporello storage to load it after page reload
|
||||||
|
globalThis.leporello.storage.set('state', state)
|
||||||
|
}
|
||||||
do_render()
|
do_render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
96
src/cmd.js
96
src/cmd.js
@@ -1,5 +1,5 @@
|
|||||||
import {map_object, map_find, filter_object, collect_nodes_with_parents, uniq}
|
import {map_object, map_find, filter_object, collect_nodes_with_parents, uniq,
|
||||||
from './utils.js'
|
set_is_eq} from './utils.js'
|
||||||
import {
|
import {
|
||||||
is_eq, is_child, ancestry, ancestry_inc, map_tree,
|
is_eq, is_child, ancestry, ancestry_inc, map_tree,
|
||||||
find_leaf, find_fn_by_location, find_node, find_error_origin_node,
|
find_leaf, find_fn_by_location, find_node, find_error_origin_node,
|
||||||
@@ -69,14 +69,19 @@ const apply_eval_result = (state, eval_result) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const run_code = (s, dirty_files) => {
|
const run_code = (s, globals) => {
|
||||||
|
const is_globals_eq = s.globals == null
|
||||||
|
? globals == null
|
||||||
|
: set_is_eq(s.globals, globals)
|
||||||
|
|
||||||
const parse_result = load_modules(s.entrypoint, module => {
|
const parse_result = load_modules(s.entrypoint, module => {
|
||||||
if(dirty_files != null && dirty_files.includes(module)) {
|
if(s.dirty_files != null && s.dirty_files.has(module)) {
|
||||||
return s.files[module]
|
return s.files[module]
|
||||||
}
|
}
|
||||||
|
|
||||||
if(s.parse_result != null) {
|
// If globals change, then errors for using undeclared identifiers may be
|
||||||
|
// no longer valid. Do not use cache
|
||||||
|
if(is_globals_eq) {
|
||||||
const result = s.parse_result.cache[module]
|
const result = s.parse_result.cache[module]
|
||||||
if(result != null) {
|
if(result != null) {
|
||||||
return result
|
return result
|
||||||
@@ -87,10 +92,18 @@ const run_code = (s, dirty_files) => {
|
|||||||
return s.files[module]
|
return s.files[module]
|
||||||
}
|
}
|
||||||
|
|
||||||
}, s.globals)
|
}, globals)
|
||||||
|
|
||||||
|
const dirty_files = new Set(
|
||||||
|
[...(s.dirty_files ?? new Set())].filter(file =>
|
||||||
|
parse_result.modules[file] == null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
...s,
|
...s,
|
||||||
|
dirty_files,
|
||||||
|
globals,
|
||||||
parse_result,
|
parse_result,
|
||||||
calltree: null,
|
calltree: null,
|
||||||
modules: null,
|
modules: null,
|
||||||
@@ -121,15 +134,7 @@ const run_code = (s, dirty_files) => {
|
|||||||
.map(i => i.node.full_import_path)
|
.map(i => i.node.full_import_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
if(
|
if(external_imports.length != 0) {
|
||||||
external_imports.length != 0
|
|
||||||
&&
|
|
||||||
(
|
|
||||||
state.external_imports_cache == null
|
|
||||||
||
|
|
||||||
external_imports.some(i => state.external_imports_cache[i] == null)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Trigger loading of external modules
|
// Trigger loading of external modules
|
||||||
return {...state,
|
return {...state,
|
||||||
loading_external_imports_state: {
|
loading_external_imports_state: {
|
||||||
@@ -138,18 +143,8 @@ const run_code = (s, dirty_files) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Modules were loaded and cached, proceed
|
// Modules were loaded and cached, proceed
|
||||||
return external_imports_loaded(
|
return external_imports_loaded(state, state)
|
||||||
state,
|
|
||||||
state,
|
|
||||||
state.external_imports_cache == null
|
|
||||||
? null
|
|
||||||
: filter_object(
|
|
||||||
state.external_imports_cache,
|
|
||||||
(module_name, module) => external_imports.includes(module_name)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const external_imports_loaded = (
|
const external_imports_loaded = (
|
||||||
@@ -168,7 +163,6 @@ const external_imports_loaded = (
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
...s,
|
...s,
|
||||||
external_imports_cache: external_imports,
|
|
||||||
loading_external_imports_state: null
|
loading_external_imports_state: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +198,7 @@ const external_imports_loaded = (
|
|||||||
state.on_deferred_call,
|
state.on_deferred_call,
|
||||||
state.calltree_changed_token,
|
state.calltree_changed_token,
|
||||||
state.io_trace,
|
state.io_trace,
|
||||||
|
state.storage,
|
||||||
)
|
)
|
||||||
|
|
||||||
if(result.then != null) {
|
if(result.then != null) {
|
||||||
@@ -252,10 +247,12 @@ const eval_modules_finished = (state, prev_state, result) => {
|
|||||||
|
|
||||||
const input = (state, code, index) => {
|
const input = (state, code, index) => {
|
||||||
const files = {...state.files, [state.current_module]: code}
|
const files = {...state.files, [state.current_module]: code}
|
||||||
const next = run_code(
|
const with_files = {
|
||||||
set_cursor_position({...state, files}, index),
|
...state,
|
||||||
[state.current_module]
|
files,
|
||||||
)
|
dirty_files: new Set([...(state.dirty_files ?? []), state.current_module])
|
||||||
|
}
|
||||||
|
const next = set_cursor_position(with_files, index)
|
||||||
const effect_save = {
|
const effect_save = {
|
||||||
type: 'write',
|
type: 'write',
|
||||||
args: [
|
args: [
|
||||||
@@ -519,12 +516,10 @@ const change_current_module = (state, current_module) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const change_entrypoint = (state, entrypoint, current_module = entrypoint) => {
|
const change_entrypoint = (state, entrypoint, current_module = entrypoint) => {
|
||||||
return run_code(
|
return {...state,
|
||||||
{...state,
|
entrypoint,
|
||||||
entrypoint,
|
current_module,
|
||||||
current_module,
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const change_html_file = (state, html_file) => {
|
const change_html_file = (state, html_file) => {
|
||||||
@@ -852,7 +847,7 @@ const on_deferred_call = (state, call, calltree_changed_token, logs) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clear_io_trace = state => {
|
const clear_io_trace = state => {
|
||||||
return run_code({...state, io_trace: null})
|
return {...state, io_trace: null}
|
||||||
}
|
}
|
||||||
|
|
||||||
const load_files = (state, dir) => {
|
const load_files = (state, dir) => {
|
||||||
@@ -892,7 +887,7 @@ const apply_entrypoint_settings = (state, entrypoint_settings) => {
|
|||||||
const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
|
const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
|
||||||
// Clear parse cache and rerun code
|
// Clear parse cache and rerun code
|
||||||
const with_dir = load_files(state, dir)
|
const with_dir = load_files(state, dir)
|
||||||
return run_code({
|
return {
|
||||||
...(
|
...(
|
||||||
entrypoint_settings == null
|
entrypoint_settings == null
|
||||||
? with_dir
|
? with_dir
|
||||||
@@ -904,30 +899,15 @@ const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
|
|||||||
// remove cache. We have to clear cache because imports of modules that are
|
// remove cache. We have to clear cache because imports of modules that are
|
||||||
// not available because project_dir is not available have errors and the
|
// not available because project_dir is not available have errors and the
|
||||||
// errors are cached
|
// errors are cached
|
||||||
parse_result: null,
|
parse_result: {...state.parse_result, cache: {}},
|
||||||
|
}
|
||||||
external_imports_cache: null,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const create_file = (state, dir, current_module) => {
|
const create_file = (state, dir, current_module) => {
|
||||||
return {...load_dir(state, dir, true), current_module}
|
return {...load_dir(state, dir, true), current_module}
|
||||||
}
|
}
|
||||||
|
|
||||||
const open_app_window = (state, globals) => {
|
const open_app_window = state => ({...state, storage: new Map()})
|
||||||
// After we reopen run window, we should reload external modules in the
|
|
||||||
// context of new window. Clear external_imports_cache
|
|
||||||
return run_code({
|
|
||||||
...state,
|
|
||||||
globals,
|
|
||||||
external_imports_cache: null,
|
|
||||||
// Bust parse result cache because list of globals may change
|
|
||||||
parse_result: null,
|
|
||||||
// Clear io trace because promises in io_trace become invalid after their
|
|
||||||
// window close
|
|
||||||
io_trace: null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
|
const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
|
||||||
const with_files = state.project_dir == null
|
const with_files = state.project_dir == null
|
||||||
@@ -938,6 +918,7 @@ const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...with_settings,
|
...with_settings,
|
||||||
|
storage: new Map(),
|
||||||
cursor_position_by_file: {[with_settings.current_module]: cursor_pos},
|
cursor_position_by_file: {[with_settings.current_module]: cursor_pos},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -945,6 +926,7 @@ const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => {
|
|||||||
export const COMMANDS = {
|
export const COMMANDS = {
|
||||||
get_initial_state,
|
get_initial_state,
|
||||||
input,
|
input,
|
||||||
|
run_code,
|
||||||
open_app_window,
|
open_app_window,
|
||||||
load_dir,
|
load_dir,
|
||||||
create_file,
|
create_file,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {exec, get_state} from '../index.js'
|
import {exec, get_state, exec_and_reload_app_window} from '../index.js'
|
||||||
import {ValueExplorer} from './value_explorer.js'
|
import {ValueExplorer} from './value_explorer.js'
|
||||||
import {stringify_for_header} from '../value_explorer_utils.js'
|
import {stringify_for_header} from '../value_explorer_utils.js'
|
||||||
import {el} from './domutils.js'
|
import {el} from './domutils.js'
|
||||||
@@ -98,7 +98,7 @@ export class Editor {
|
|||||||
normalize_events(this.ace_editor, {
|
normalize_events(this.ace_editor, {
|
||||||
on_change: () => {
|
on_change: () => {
|
||||||
try {
|
try {
|
||||||
exec('input', this.ace_editor.getValue(), this.get_cursor_position())
|
exec_and_reload_app_window('input', this.ace_editor.getValue(), this.get_cursor_position())
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// Do not throw Error to ACE because it breaks typing
|
// Do not throw Error to ACE because it breaks typing
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
exec,
|
exec,
|
||||||
get_state,
|
get_state,
|
||||||
open_directory,
|
open_directory,
|
||||||
reload_app_window,
|
exec_and_reload_app_window,
|
||||||
close_directory,
|
close_directory,
|
||||||
} from '../index.js'
|
} from '../index.js'
|
||||||
|
|
||||||
@@ -20,16 +20,14 @@ export class Files {
|
|||||||
this.render(get_state())
|
this.render(get_state())
|
||||||
}
|
}
|
||||||
|
|
||||||
change_entrypoint(e) {
|
change_entrypoint(entrypoint, current_module) {
|
||||||
const file = e.target.value
|
exec_and_reload_app_window('change_entrypoint', entrypoint, current_module)
|
||||||
exec('change_entrypoint', file)
|
|
||||||
this.ui.editor.focus()
|
this.ui.editor.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
change_html_file(e) {
|
change_html_file(e) {
|
||||||
const html_file = e.target.value
|
const html_file = e.target.value
|
||||||
exec('change_html_file', html_file)
|
exec_and_reload_app_window('change_html_file', html_file)
|
||||||
reload_app_window(get_state())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +117,7 @@ export class Files {
|
|||||||
name: 'js_entrypoint',
|
name: 'js_entrypoint',
|
||||||
value: file.path,
|
value: file.path,
|
||||||
checked: state.entrypoint == file.path,
|
checked: state.entrypoint == file.path,
|
||||||
change: e => this.change_entrypoint(e),
|
change: e => this.change_entrypoint(e.target.value),
|
||||||
click: e => e.stopPropagation(),
|
click: e => e.stopPropagation(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -210,9 +208,9 @@ export class Files {
|
|||||||
// Reload all files for simplicity
|
// Reload all files for simplicity
|
||||||
open_dir(false).then(dir => {
|
open_dir(false).then(dir => {
|
||||||
if(is_dir) {
|
if(is_dir) {
|
||||||
exec('load_dir', dir, true)
|
exec_and_reload_app_window('load_dir', dir, true)
|
||||||
} else {
|
} else {
|
||||||
exec('create_file', dir, path)
|
exec_and_reload_app_window('create_file', dir, path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -229,7 +227,7 @@ export class Files {
|
|||||||
if(file.path == null) {
|
if(file.path == null) {
|
||||||
// root of examples dir, do nothing
|
// root of examples dir, do nothing
|
||||||
} else if(file.path == '') {
|
} else if(file.path == '') {
|
||||||
exec('change_entrypoint', '')
|
this.change_entrypoint('')
|
||||||
} else {
|
} else {
|
||||||
const find_node = n =>
|
const find_node = n =>
|
||||||
n.path == file.path
|
n.path == file.path
|
||||||
@@ -244,8 +242,7 @@ export class Files {
|
|||||||
// in examples mode, on click file we also change entrypoint for
|
// in examples mode, on click file we also change entrypoint for
|
||||||
// simplicity
|
// simplicity
|
||||||
const example = examples.find(e => e.path == example_dir.path)
|
const example = examples.find(e => e.path == example_dir.path)
|
||||||
exec(
|
this.change_entrypoint(
|
||||||
'change_entrypoint',
|
|
||||||
example.entrypoint,
|
example.entrypoint,
|
||||||
file.kind == 'directory' ? undefined : file.path
|
file.kind == 'directory' ? undefined : file.path
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {exec, get_state, open_app_window} from '../index.js'
|
import {exec, get_state, open_app_window, exec_and_reload_app_window} from '../index.js'
|
||||||
import {Editor} from './editor.js'
|
import {Editor} from './editor.js'
|
||||||
import {Files} from './files.js'
|
import {Files} from './files.js'
|
||||||
import {CallTree} from './calltree.js'
|
import {CallTree} from './calltree.js'
|
||||||
@@ -73,7 +73,7 @@ export class UI {
|
|||||||
el('a', {
|
el('a', {
|
||||||
'class': 'statusbar_action first',
|
'class': 'statusbar_action first',
|
||||||
href: 'javascript: void(0)',
|
href: 'javascript: void(0)',
|
||||||
click: () => exec('clear_io_trace')
|
click: () => this.clear_io_trace(),
|
||||||
},
|
},
|
||||||
'Clear IO trace (F6)'
|
'Clear IO trace (F6)'
|
||||||
),
|
),
|
||||||
@@ -196,6 +196,10 @@ export class UI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear_io_trace() {
|
||||||
|
exec_and_reload_app_window('clear_io_trace')
|
||||||
|
}
|
||||||
|
|
||||||
open_app_window() {
|
open_app_window() {
|
||||||
this.toggle_open_app_window_tooltip(false)
|
this.toggle_open_app_window_tooltip(false)
|
||||||
localStorage.onboarding_open_app_window = true
|
localStorage.onboarding_open_app_window = true
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ export const eval_modules = (
|
|||||||
on_deferred_call,
|
on_deferred_call,
|
||||||
calltree_changed_token,
|
calltree_changed_token,
|
||||||
io_trace,
|
io_trace,
|
||||||
|
storage,
|
||||||
) => {
|
) => {
|
||||||
// TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc,
|
// TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc,
|
||||||
// __await_start, __await_finish, __Multiversion, __create_array, __create_object
|
// __await_start, __await_finish, __Multiversion, __create_array, __create_object
|
||||||
@@ -498,6 +499,8 @@ export const eval_modules = (
|
|||||||
is_toplevel_call: true,
|
is_toplevel_call: true,
|
||||||
|
|
||||||
window: globalThis.app_window,
|
window: globalThis.app_window,
|
||||||
|
|
||||||
|
storage,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = run(module_fns, cxt, io_trace)
|
const result = run(module_fns, cxt, io_trace)
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const add_trivial_definition = node => {
|
|||||||
* will be assigned by the time the closures would be called
|
* will be assigned by the time the closures would be called
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const DEFAULT_GLOBALS = new Set(['leporello']) // Leporello.js API
|
||||||
|
|
||||||
export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => {
|
export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => {
|
||||||
|
|
||||||
// sanity check
|
// sanity check
|
||||||
@@ -94,7 +96,7 @@ export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, m
|
|||||||
} else {
|
} else {
|
||||||
const definition = scope[ast.value]
|
const definition = scope[ast.value]
|
||||||
if(definition == null){
|
if(definition == null){
|
||||||
if(globals.has(ast.value)) {
|
if(globals.has(ast.value) || DEFAULT_GLOBALS.has(ast.value)) {
|
||||||
return {node: {...ast, definition: 'global'}, undeclared: null, closed: new Set()}
|
return {node: {...ast, definition: 'global'}, undeclared: null, closed: new Set()}
|
||||||
} else {
|
} else {
|
||||||
return {node: ast, undeclared: [ast], closed: new Set()}
|
return {node: ast, undeclared: [ast], closed: new Set()}
|
||||||
|
|||||||
17
src/index.js
17
src/index.js
@@ -52,7 +52,7 @@ const get_html_url = state => {
|
|||||||
const on_window_load = w => {
|
const on_window_load = w => {
|
||||||
init_window_service_worker(w)
|
init_window_service_worker(w)
|
||||||
exec(
|
exec(
|
||||||
'open_app_window',
|
'run_code',
|
||||||
new Set(Object.getOwnPropertyNames(w))
|
new Set(Object.getOwnPropertyNames(w))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,7 @@ const open_run_iframe = (state) => {
|
|||||||
export const open_app_window = state => {
|
export const open_app_window = state => {
|
||||||
// TODO set_error_handler? Or we dont need to set_error_handler for child
|
// TODO set_error_handler? Or we dont need to set_error_handler for child
|
||||||
// window because error is always caught by parent window handler?
|
// window because error is always caught by parent window handler?
|
||||||
|
exec('open_app_window')
|
||||||
globalThis.app_window.close()
|
globalThis.app_window.close()
|
||||||
globalThis.app_window = open(get_html_url(state))
|
globalThis.app_window = open(get_html_url(state))
|
||||||
init_app_window(globalThis.app_window)
|
init_app_window(globalThis.app_window)
|
||||||
@@ -139,10 +140,8 @@ const init_app_window = w => {
|
|||||||
add_load_handler()
|
add_load_handler()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reload_app_window = state => {
|
const reload_app_window = state => {
|
||||||
// TODO after window location reload, open_app_window command will be fired.
|
// after window location reload, `run_code` command will be fired.
|
||||||
// Maybe we should have separate commands for open_app_window and
|
|
||||||
// reload_app_window?
|
|
||||||
globalThis.app_window.location = get_html_url(state)
|
globalThis.app_window.location = get_html_url(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,19 +153,23 @@ const get_entrypoint_settings = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const exec_and_reload_app_window = (...exec_args) => {
|
||||||
|
exec(...exec_args)
|
||||||
|
reload_app_window(get_state())
|
||||||
|
}
|
||||||
|
|
||||||
export const open_directory = () => {
|
export const open_directory = () => {
|
||||||
if(globalThis.showDirectoryPicker == null) {
|
if(globalThis.showDirectoryPicker == null) {
|
||||||
throw new Error('Your browser is not supporting File System Access API')
|
throw new Error('Your browser is not supporting File System Access API')
|
||||||
}
|
}
|
||||||
open_dir(true).then(dir => {
|
open_dir(true).then(dir => {
|
||||||
exec('load_dir', dir, true, get_entrypoint_settings())
|
exec_and_reload_app_window('load_dir', dir, true, get_entrypoint_settings())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const close_directory = async () => {
|
export const close_directory = async () => {
|
||||||
close_dir()
|
close_dir()
|
||||||
exec('load_dir', await examples_dir_promise, false, get_entrypoint_settings())
|
exec_and_reload_app_window('load_dir', await examples_dir_promise, false, get_entrypoint_settings())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,18 +76,25 @@ const make_patched_method = (window, original, name, use_context) => {
|
|||||||
: original.apply(this, args)
|
: original.apply(this, args)
|
||||||
|
|
||||||
if(value?.[Symbol.toStringTag] == 'Promise') {
|
if(value?.[Symbol.toStringTag] == 'Promise') {
|
||||||
// TODO use __original_then, not finally which calls
|
value = value
|
||||||
// patched 'then'?
|
.then(val => {
|
||||||
value = value.finally(() => {
|
value.status = {ok: true, value: val}
|
||||||
if(cxt_copy != cxt) {
|
return val
|
||||||
return
|
})
|
||||||
}
|
.catch(error => {
|
||||||
if(cxt.io_trace_is_replay_aborted) {
|
value.status = {ok: true, error}
|
||||||
// Non necessary
|
throw error
|
||||||
return
|
})
|
||||||
}
|
.finally(() => {
|
||||||
cxt.io_trace.push({type: 'resolution', index})
|
if(cxt_copy != cxt) {
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
if(cxt.io_trace_is_replay_aborted) {
|
||||||
|
// Non necessary
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cxt.io_trace.push({type: 'resolution', index})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
@@ -150,10 +157,11 @@ const make_patched_method = (window, original, name, use_context) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if(next_resolution != null && !cxt.io_trace_resolver_is_set) {
|
if(next_resolution != null && !cxt.io_trace_resolver_is_set) {
|
||||||
const original_setTimeout = cxt.window.setTimeout.__original
|
|
||||||
cxt.io_trace_resolver_is_set = true
|
cxt.io_trace_resolver_is_set = true
|
||||||
|
|
||||||
original_setTimeout(() => {
|
// use setTimeout function from host window (because this module was
|
||||||
|
// loaded as `external` by host window)
|
||||||
|
setTimeout(() => {
|
||||||
if(cxt_copy != cxt) {
|
if(cxt_copy != cxt) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -180,14 +188,22 @@ const make_patched_method = (window, original, name, use_context) => {
|
|||||||
cxt.io_trace[cxt.io_trace_index].type == 'resolution'
|
cxt.io_trace[cxt.io_trace_index].type == 'resolution'
|
||||||
) {
|
) {
|
||||||
const resolution = cxt.io_trace[cxt.io_trace_index]
|
const resolution = cxt.io_trace[cxt.io_trace_index]
|
||||||
const resolver = cxt.io_trace_resolvers.get(resolution.index)
|
const {resolve, reject} = cxt.io_trace_resolvers.get(resolution.index)
|
||||||
|
|
||||||
cxt.io_trace_index++
|
cxt.io_trace_index++
|
||||||
|
|
||||||
if(cxt.io_trace[resolution.index].name == 'setTimeout') {
|
if(cxt.io_trace[resolution.index].name == 'setTimeout') {
|
||||||
resolver()
|
resolve()
|
||||||
} else {
|
} else {
|
||||||
resolver(cxt.io_trace[resolution.index].value)
|
const promise = cxt.io_trace[resolution.index].value
|
||||||
|
if(promise.status == null) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
if(promise.status.ok) {
|
||||||
|
resolve(promise.status.value)
|
||||||
|
} else {
|
||||||
|
reject(promise.status.error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,12 +219,12 @@ const make_patched_method = (window, original, name, use_context) => {
|
|||||||
// trace) and instanceof would not work
|
// trace) and instanceof would not work
|
||||||
if(call.value?.[Symbol.toStringTag] == 'Promise') {
|
if(call.value?.[Symbol.toStringTag] == 'Promise') {
|
||||||
// Always make promise originate from app_window
|
// Always make promise originate from app_window
|
||||||
return new cxt.window.Promise(resolve => {
|
return new cxt.window.Promise((resolve, reject) => {
|
||||||
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, resolve)
|
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, {resolve, reject})
|
||||||
})
|
})
|
||||||
} else if(name == 'setTimeout') {
|
} else if(name == 'setTimeout') {
|
||||||
const timeout_cb = args[0]
|
const timeout_cb = args[0]
|
||||||
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, timeout_cb)
|
cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, {resolve: timeout_cb})
|
||||||
return call.value
|
return call.value
|
||||||
} else {
|
} else {
|
||||||
return call.value
|
return call.value
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ const gen_to_promise = gen_fn => {
|
|||||||
if(result.done){
|
if(result.done){
|
||||||
return result.value
|
return result.value
|
||||||
} else {
|
} else {
|
||||||
// If promise
|
if(result.value?.[Symbol.toStringTag] == 'Promise') {
|
||||||
if(result.value?.then != null) {
|
return result.value.then(
|
||||||
return result.value.__original_then(
|
|
||||||
value => next(gen.next(value)),
|
value => next(gen.next(value)),
|
||||||
error => next(gen.throw(error)),
|
error => next(gen.throw(error)),
|
||||||
)
|
)
|
||||||
@@ -36,7 +35,7 @@ const gen_to_promise = gen_fn => {
|
|||||||
|
|
||||||
const make_promise_with_rejector = cxt => {
|
const make_promise_with_rejector = cxt => {
|
||||||
let rejector
|
let rejector
|
||||||
const p = new cxt.window.Promise(r => rejector = r)
|
const p = new Promise(r => rejector = r)
|
||||||
return [p, rejector]
|
return [p, rejector]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +106,8 @@ const do_run = function*(module_fns, cxt, io_trace){
|
|||||||
create_array,
|
create_array,
|
||||||
create_object,
|
create_object,
|
||||||
)
|
)
|
||||||
if(result instanceof cxt.window.Promise) {
|
if(result?.[Symbol.toStringTag] == 'Promise') {
|
||||||
yield cxt.window.Promise.race([replay_aborted_promise, result])
|
yield Promise.race([replay_aborted_promise, result])
|
||||||
} else {
|
} else {
|
||||||
yield result
|
yield result
|
||||||
}
|
}
|
||||||
@@ -144,7 +143,10 @@ export const run = gen_to_promise(function*(module_fns, cxt, io_trace) {
|
|||||||
if(!cxt.window.__is_initialized) {
|
if(!cxt.window.__is_initialized) {
|
||||||
defineMultiversion(cxt.window)
|
defineMultiversion(cxt.window)
|
||||||
apply_io_patches(cxt.window)
|
apply_io_patches(cxt.window)
|
||||||
|
inject_leporello_api(cxt)
|
||||||
cxt.window.__is_initialized = true
|
cxt.window.__is_initialized = true
|
||||||
|
} else {
|
||||||
|
throw new Error('illegal state')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = yield* do_run(module_fns, cxt, io_trace)
|
const result = yield* do_run(module_fns, cxt, io_trace)
|
||||||
@@ -154,14 +156,21 @@ export const run = gen_to_promise(function*(module_fns, cxt, io_trace) {
|
|||||||
result.rt_cxt.is_recording_deferred_calls = false
|
result.rt_cxt.is_recording_deferred_calls = false
|
||||||
|
|
||||||
// run again without io trace
|
// run again without io trace
|
||||||
|
// TODO reload app_window before second run
|
||||||
return yield* do_run(module_fns, cxt, null)
|
return yield* do_run(module_fns, cxt, null)
|
||||||
} else {
|
} else {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inject_leporello_api = cxt => {
|
||||||
|
cxt.window.leporello = { storage: cxt.storage }
|
||||||
|
}
|
||||||
|
|
||||||
const apply_promise_patch = cxt => {
|
const apply_promise_patch = cxt => {
|
||||||
|
if(cxt.window.Promise.prototype.__original_then != null) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
const original_then = cxt.window.Promise.prototype.then
|
const original_then = cxt.window.Promise.prototype.then
|
||||||
cxt.window.Promise.prototype.__original_then = cxt.window.Promise.prototype.then
|
cxt.window.Promise.prototype.__original_then = cxt.window.Promise.prototype.then
|
||||||
|
|
||||||
@@ -197,6 +206,7 @@ const apply_promise_patch = cxt => {
|
|||||||
|
|
||||||
const remove_promise_patch = cxt => {
|
const remove_promise_patch = cxt => {
|
||||||
cxt.window.Promise.prototype.then = cxt.window.Promise.prototype.__original_then
|
cxt.window.Promise.prototype.then = cxt.window.Promise.prototype.__original_then
|
||||||
|
delete cxt.window.Promise.prototype.__original_then
|
||||||
}
|
}
|
||||||
|
|
||||||
export const set_record_call = cxt => {
|
export const set_record_call = cxt => {
|
||||||
@@ -339,7 +349,7 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
|
|||||||
try {
|
try {
|
||||||
value = fn(...args)
|
value = fn(...args)
|
||||||
ok = true
|
ok = true
|
||||||
if(value instanceof cxt.window.Promise) {
|
if(value?.[Symbol.toStringTag] == 'Promise') {
|
||||||
set_record_call(cxt)
|
set_record_call(cxt)
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
@@ -485,7 +495,7 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
|
|||||||
value = undefined
|
value = undefined
|
||||||
}
|
}
|
||||||
ok = true
|
ok = true
|
||||||
if(value instanceof cxt.window.Promise) {
|
if(value?.[Symbol.toStringTag] == 'Promise') {
|
||||||
set_record_call(cxt)
|
set_record_call(cxt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export const set_push = (x,y) => new Set([...x, y])
|
|||||||
|
|
||||||
export const set_union = (x,y) => new Set([...x, ...y])
|
export const set_union = (x,y) => new Set([...x, ...y])
|
||||||
|
|
||||||
|
export const set_is_eq = (a, b) =>
|
||||||
|
a.size === b.size && [...a].every(value => b.has(value))
|
||||||
|
|
||||||
export const set_diff = (x,y) => {
|
export const set_diff = (x,y) => {
|
||||||
return new Set([...x].filter(el => !y.has(el)))
|
return new Set([...x].filter(el => !y.has(el)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ const isError = object =>
|
|||||||
||
|
||
|
||||||
object instanceof globalThis.app_window.Error
|
object instanceof globalThis.app_window.Error
|
||||||
|
|
||||||
const isPromise = object =>
|
const isPromise = object => object?.[Symbol.toStringTag] == 'Promise'
|
||||||
object instanceof globalThis.app_window.Promise
|
|
||||||
|
|
||||||
// Override behaviour for Date, becase Date has toJSON defined
|
// Override behaviour for Date, becase Date has toJSON defined
|
||||||
const isDate = object =>
|
const isDate = object =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {tests} from './test.js'
|
import {tests} from './test.js'
|
||||||
|
|
||||||
// external
|
// external
|
||||||
import {run} from './run_utils.js'
|
import {run} from './utils.js'
|
||||||
|
|
||||||
await run(tests)
|
await run(tests)
|
||||||
|
|||||||
@@ -8,61 +8,47 @@
|
|||||||
*/
|
*/
|
||||||
globalThis.Response
|
globalThis.Response
|
||||||
|
|
||||||
export const run = tests => {
|
if(globalThis.process != null) {
|
||||||
// Runs test, return failure or null if not failed
|
globalThis.NodeVM = await import('node:vm')
|
||||||
const run_test = t => {
|
}
|
||||||
return Promise.resolve().then(t.test)
|
|
||||||
.then(() => null)
|
|
||||||
.catch(e => {
|
|
||||||
if(globalThis.process != null) {
|
|
||||||
// In node.js runner, fail fast
|
|
||||||
console.error('Failed: ' + t.message)
|
|
||||||
throw e
|
|
||||||
} else {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not run in node, then dont apply filter
|
let iframe
|
||||||
const filter = globalThis.process && globalThis.process.argv[2]
|
|
||||||
|
|
||||||
if(filter == null) {
|
export function create_app_window() {
|
||||||
|
if(globalThis.process != null) {
|
||||||
|
// We are in node.js
|
||||||
|
// `NodeVM` was preloaded earlier
|
||||||
|
|
||||||
const only = tests.find(t => t.only)
|
const context = globalThis.NodeVM.createContext({
|
||||||
const tests_to_run = only == null ? tests : [only]
|
|
||||||
|
|
||||||
// Exec each test. After all tests are done, we rethrow first error if
|
process,
|
||||||
// any. So we will mark root calltree node if one of tests failed
|
|
||||||
return tests_to_run.reduce(
|
|
||||||
(failureP, t) =>
|
|
||||||
Promise.resolve(failureP).then(failure =>
|
|
||||||
run_test(t).then(next_failure => failure ?? next_failure)
|
|
||||||
)
|
|
||||||
,
|
|
||||||
null
|
|
||||||
).then(failure => {
|
|
||||||
|
|
||||||
if(failure != null) {
|
// for some reason URL is not available inside VM
|
||||||
throw failure
|
URL,
|
||||||
} else {
|
|
||||||
if(globalThis.process != null) {
|
|
||||||
console.log('Ok')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
console,
|
||||||
|
setTimeout,
|
||||||
|
// break fetch because we dont want it to be accidentally called in unit test
|
||||||
|
fetch: () => {
|
||||||
|
console.error('Error! fetch called')
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
const get_global_object = globalThis.NodeVM.compileFunction(
|
||||||
|
'return this',
|
||||||
|
[], // args
|
||||||
|
{parsingContext: context}
|
||||||
|
)
|
||||||
|
|
||||||
|
return get_global_object()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const test = tests.find(t => t.message.includes(filter))
|
// We are in browser
|
||||||
if(test == null) {
|
if(iframe != null) {
|
||||||
throw new Error('test not found')
|
globalThis.document.body.removeChild(iframe)
|
||||||
} else {
|
|
||||||
return run_test(test).then(() => {
|
|
||||||
if(globalThis.process != null) {
|
|
||||||
console.log('Ok')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
iframe = globalThis.document.createElement('iframe')
|
||||||
|
document.body.appendChild(iframe)
|
||||||
|
return iframe.contentWindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
249
test/test.js
249
test/test.js
@@ -23,12 +23,12 @@ import {
|
|||||||
assert_code_error, assert_code_error_async,
|
assert_code_error, assert_code_error_async,
|
||||||
assert_versioned_value, assert_value_explorer, assert_selection,
|
assert_versioned_value, assert_value_explorer, assert_selection,
|
||||||
parse_modules,
|
parse_modules,
|
||||||
|
run_code,
|
||||||
test_initial_state, test_initial_state_async,
|
test_initial_state, test_initial_state_async,
|
||||||
test_deferred_calls_state,
|
test_deferred_calls_state,
|
||||||
print_debug_ct_node,
|
print_debug_ct_node,
|
||||||
command_input_async,
|
input,
|
||||||
patch_builtin,
|
input_async,
|
||||||
original_setTimeout,
|
|
||||||
} from './utils.js'
|
} from './utils.js'
|
||||||
|
|
||||||
export const tests = [
|
export const tests = [
|
||||||
@@ -1012,7 +1012,8 @@ export const tests = [
|
|||||||
const spoil_file = {...s, files: {...s.files, 'c': ',,,'}}
|
const spoil_file = {...s, files: {...s.files, 'c': ',,,'}}
|
||||||
|
|
||||||
// change module ''
|
// change module ''
|
||||||
const {state: s2} = COMMANDS.input(spoil_file, 'import {c} from "c"', 0)
|
const {state: s2} = input(spoil_file, 'import {c} from "c"', 0)
|
||||||
|
assert_equal(s2.dirty_files, new Set())
|
||||||
|
|
||||||
assert_equal(s2.parse_result.ok, true)
|
assert_equal(s2.parse_result.ok, true)
|
||||||
}),
|
}),
|
||||||
@@ -1110,7 +1111,7 @@ export const tests = [
|
|||||||
|
|
||||||
const index = edited.indexOf('foo_var')
|
const index = edited.indexOf('foo_var')
|
||||||
|
|
||||||
const {state, effects} = COMMANDS.input(
|
const {state, effects} = input(
|
||||||
initial,
|
initial,
|
||||||
edited,
|
edited,
|
||||||
index
|
index
|
||||||
@@ -1164,39 +1165,6 @@ export const tests = [
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
test('module external cache', () => {
|
|
||||||
const code = `
|
|
||||||
// external
|
|
||||||
import {foo_var} from 'foo.js'
|
|
||||||
console.log(foo_var)
|
|
||||||
`
|
|
||||||
const initial = test_initial_state(code)
|
|
||||||
|
|
||||||
const next = COMMANDS.external_imports_loaded(initial, initial, {
|
|
||||||
'foo.js': {
|
|
||||||
ok: true,
|
|
||||||
module: {
|
|
||||||
'foo_var': 'foo_value'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const edited = `
|
|
||||||
// external
|
|
||||||
import {foo_var} from 'foo.js'
|
|
||||||
foo_var
|
|
||||||
`
|
|
||||||
|
|
||||||
const {state} = COMMANDS.input(
|
|
||||||
next,
|
|
||||||
edited,
|
|
||||||
edited.lastIndexOf('foo_var'),
|
|
||||||
)
|
|
||||||
|
|
||||||
// If cache was not used then effects will be `load_external_imports`
|
|
||||||
assert_equal(state.value_explorer.result.value, 'foo_value')
|
|
||||||
}),
|
|
||||||
|
|
||||||
test('module external cache error bug', () => {
|
test('module external cache error bug', () => {
|
||||||
const code = `
|
const code = `
|
||||||
// external
|
// external
|
||||||
@@ -1221,7 +1189,7 @@ export const tests = [
|
|||||||
`
|
`
|
||||||
|
|
||||||
// edit code
|
// edit code
|
||||||
const {state} = COMMANDS.input(
|
const {state} = input(
|
||||||
next,
|
next,
|
||||||
edited,
|
edited,
|
||||||
edited.lastIndexOf('foo_var'),
|
edited.lastIndexOf('foo_var'),
|
||||||
@@ -1259,7 +1227,7 @@ export const tests = [
|
|||||||
const edited = ``
|
const edited = ``
|
||||||
|
|
||||||
// edit code
|
// edit code
|
||||||
const {state, effects} = COMMANDS.input(
|
const {state, effects} = input(
|
||||||
next,
|
next,
|
||||||
edited,
|
edited,
|
||||||
0,
|
0,
|
||||||
@@ -2000,7 +1968,7 @@ const y = x()`
|
|||||||
|
|
||||||
x(2);
|
x(2);
|
||||||
`
|
`
|
||||||
const s3 = COMMANDS.input(s2, invalid, invalid.indexOf('return')).state
|
const s3 = input(s2, invalid, invalid.indexOf('return')).state
|
||||||
|
|
||||||
const edited = `
|
const edited = `
|
||||||
const x = foo => {
|
const x = foo => {
|
||||||
@@ -2010,7 +1978,7 @@ const y = x()`
|
|||||||
x(2);
|
x(2);
|
||||||
`
|
`
|
||||||
|
|
||||||
const n = COMMANDS.input(s3, edited, edited.indexOf('return')).state
|
const n = input(s3, edited, edited.indexOf('return')).state
|
||||||
|
|
||||||
const res = find_leaf(active_frame(n), edited.indexOf('*'))
|
const res = find_leaf(active_frame(n), edited.indexOf('*'))
|
||||||
|
|
||||||
@@ -2044,7 +2012,7 @@ const y = x()`
|
|||||||
[1,2,3].map(x)
|
[1,2,3].map(x)
|
||||||
`
|
`
|
||||||
|
|
||||||
const e = COMMANDS.input(s4, edited, edited.indexOf('2')).state
|
const e = input(s4, edited, edited.indexOf('2')).state
|
||||||
|
|
||||||
const active = active_frame(e)
|
const active = active_frame(e)
|
||||||
|
|
||||||
@@ -2070,7 +2038,7 @@ const y = x()`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const {state: s2} = COMMANDS.input(s1, edited, edited.indexOf('1'))
|
const {state: s2} = input(s1, edited, edited.indexOf('1'))
|
||||||
const s3 = COMMANDS.move_cursor(s2, edited.indexOf('import'))
|
const s3 = COMMANDS.move_cursor(s2, edited.indexOf('import'))
|
||||||
assert_equal(s3.value_explorer.result.value.x, 1)
|
assert_equal(s3.value_explorer.result.value.x, 1)
|
||||||
}),
|
}),
|
||||||
@@ -2098,7 +2066,7 @@ const y = x()`
|
|||||||
x()
|
x()
|
||||||
`
|
`
|
||||||
|
|
||||||
const e = COMMANDS.input(s3, edited, edited.indexOf('123')).state
|
const e = input(s3, edited, edited.indexOf('123')).state
|
||||||
|
|
||||||
assert_equal(e.active_calltree_node.toplevel, true)
|
assert_equal(e.active_calltree_node.toplevel, true)
|
||||||
}),
|
}),
|
||||||
@@ -2111,7 +2079,7 @@ const y = x()`
|
|||||||
}),
|
}),
|
||||||
'x'
|
'x'
|
||||||
)
|
)
|
||||||
const e = COMMANDS.input(s1, 'export const x = 2', 0).state
|
const e = input(s1, 'export const x = 2', 0).state
|
||||||
assert_equal(e.current_calltree_node.module, '')
|
assert_equal(e.current_calltree_node.module, '')
|
||||||
assert_equal(e.active_calltree_node, null)
|
assert_equal(e.active_calltree_node, null)
|
||||||
}),
|
}),
|
||||||
@@ -2143,7 +2111,7 @@ const y = x()`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const moved = COMMANDS.move_cursor(s3, code.indexOf('2'))
|
const moved = COMMANDS.move_cursor(s3, code.indexOf('2'))
|
||||||
const e = COMMANDS.input(moved, edited, edited.indexOf('3')).state
|
const e = input(moved, edited, edited.indexOf('3')).state
|
||||||
assert_equal(e.active_calltree_node, null)
|
assert_equal(e.active_calltree_node, null)
|
||||||
assert_equal(e.current_calltree_node.toplevel, true)
|
assert_equal(e.current_calltree_node.toplevel, true)
|
||||||
}),
|
}),
|
||||||
@@ -2156,7 +2124,7 @@ const y = x()`
|
|||||||
x()
|
x()
|
||||||
`
|
`
|
||||||
const i = test_initial_state(code)
|
const i = test_initial_state(code)
|
||||||
const edited = COMMANDS.input(i, code.replace('1', '100'), code.indexOf('1')).state
|
const edited = input(i, code.replace('1', '100'), code.indexOf('1')).state
|
||||||
const left = COMMANDS.calltree.arrow_left(edited)
|
const left = COMMANDS.calltree.arrow_left(edited)
|
||||||
assert_equal(left.active_calltree_node.toplevel, true)
|
assert_equal(left.active_calltree_node.toplevel, true)
|
||||||
}),
|
}),
|
||||||
@@ -3550,12 +3518,12 @@ const y = x()`
|
|||||||
`
|
`
|
||||||
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
||||||
|
|
||||||
const input = COMMANDS.input(i, code, 0).state
|
const state = input(i, code, 0).state
|
||||||
|
|
||||||
// Make deferred call, calling fn from previous code
|
// Make deferred call, calling fn from previous code
|
||||||
i.modules[''].fn(1)
|
i.modules[''].fn(1)
|
||||||
|
|
||||||
const result = on_deferred_call(input)
|
const result = on_deferred_call(state)
|
||||||
|
|
||||||
// deferred calls must be null, because deferred calls from previous executions
|
// deferred calls must be null, because deferred calls from previous executions
|
||||||
// must be discarded
|
// must be discarded
|
||||||
@@ -3911,7 +3879,7 @@ const y = x()`
|
|||||||
}
|
}
|
||||||
await f()
|
await f()
|
||||||
`
|
`
|
||||||
const next = await command_input_async(i, code2, code2.indexOf('1'))
|
const next = await input_async(i, code2, code2.indexOf('1'))
|
||||||
assert_equal(next.active_calltree_node.fn.name, 'f')
|
assert_equal(next.active_calltree_node.fn.name, 'f')
|
||||||
assert_equal(next.value_explorer.result.value, 1)
|
assert_equal(next.value_explorer.result.value, 1)
|
||||||
}),
|
}),
|
||||||
@@ -3955,22 +3923,21 @@ const y = x()`
|
|||||||
`
|
`
|
||||||
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
||||||
|
|
||||||
await i.eval_modules_state.promise.__original_then(result => {
|
const result = await i.eval_modules_state.promise
|
||||||
const s = COMMANDS.eval_modules_finished(
|
|
||||||
i,
|
|
||||||
i,
|
|
||||||
result,
|
|
||||||
i.eval_modules_state.node,
|
|
||||||
i.eval_modules_state.toplevel
|
|
||||||
)
|
|
||||||
|
|
||||||
// Make deferred call
|
const s = COMMANDS.eval_modules_finished(
|
||||||
s.modules[''].fn()
|
i,
|
||||||
const state = on_deferred_call(s)
|
i,
|
||||||
assert_equal(get_deferred_calls(state).length, 1)
|
result,
|
||||||
assert_equal(get_deferred_calls(state)[0].value, 1)
|
i.eval_modules_state.node,
|
||||||
})
|
i.eval_modules_state.toplevel
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make deferred call
|
||||||
|
s.modules[''].fn()
|
||||||
|
const state = on_deferred_call(s)
|
||||||
|
assert_equal(get_deferred_calls(state).length, 1)
|
||||||
|
assert_equal(get_deferred_calls(state)[0].value, 1)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
test('async/await await argument bug', async () => {
|
test('async/await await argument bug', async () => {
|
||||||
@@ -3991,101 +3958,94 @@ const y = x()`
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
test('record io', () => {
|
test('record io', () => {
|
||||||
// Patch Math.random to always return 1
|
let app_window_patches
|
||||||
patch_builtin('random', () => 1)
|
|
||||||
|
|
||||||
const initial = test_initial_state(`
|
// Patch Math.random to always return 1
|
||||||
const x = Math.random()
|
app_window_patches = {'Math.random': () => 1}
|
||||||
`)
|
|
||||||
|
const initial = test_initial_state(`const x = Math.random()`, undefined, {
|
||||||
|
app_window_patches
|
||||||
|
})
|
||||||
|
|
||||||
// Now call to Math.random is cached, break it to ensure it was not called
|
// Now call to Math.random is cached, break it to ensure it was not called
|
||||||
// on next run
|
// on next run
|
||||||
patch_builtin('random', () => { throw 'fail' })
|
app_window_patches = {'Math.random': () => { throw 'fail' }}
|
||||||
|
|
||||||
const next = COMMANDS.input(initial, `const x = Math.random()*2`, 0).state
|
const next = input(initial, `const x = Math.random()*2`, 0, {app_window_patches}).state
|
||||||
assert_equal(next.value_explorer.result.value, 2)
|
assert_equal(next.value_explorer.result.value, 2)
|
||||||
assert_equal(next.rt_cxt.io_trace_index, 1)
|
assert_equal(next.rt_cxt.io_trace_index, 1)
|
||||||
|
|
||||||
// Patch Math.random to return 2.
|
// Patch Math.random to return 2.
|
||||||
// TODO The first call to Math.random() is cached with value 1, and the
|
// TODO The first call to Math.random() is cached with value 1, and the
|
||||||
// second shoud return 2
|
// second shoud return 2
|
||||||
patch_builtin('random', () => 2)
|
app_window_patches = {'Math.random': () => 2}
|
||||||
const replay_failed = COMMANDS.input(
|
const replay_failed = input(
|
||||||
initial,
|
initial,
|
||||||
`const x = Math.random() + Math.random()`,
|
`const x = Math.random() + Math.random()`,
|
||||||
0
|
0,
|
||||||
|
{app_window_patches}
|
||||||
).state
|
).state
|
||||||
|
|
||||||
// TODO must reuse first cached call?
|
// TODO must reuse first cached call?
|
||||||
assert_equal(replay_failed.value_explorer.result.value, 4)
|
assert_equal(replay_failed.value_explorer.result.value, 4)
|
||||||
|
|
||||||
// Remove patch
|
|
||||||
patch_builtin('random', null)
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
test('record io trace discarded if args does not match', async () => {
|
test('record io trace discarded if args does not match', async () => {
|
||||||
// Patch fetch
|
// Patch fetch
|
||||||
patch_builtin('fetch', async () => 'first')
|
let app_window_patches
|
||||||
|
app_window_patches = {fetch: async () => 'first'}
|
||||||
|
|
||||||
const initial = await test_initial_state_async(`
|
const initial = await test_initial_state_async(`
|
||||||
console.log(await fetch('url', {method: 'GET'}))
|
console.log(await fetch('url', {method: 'GET'}))
|
||||||
`)
|
`, undefined, {app_window_patches})
|
||||||
assert_equal(initial.logs.logs[0].args[0], 'first')
|
assert_equal(initial.logs.logs[0].args[0], 'first')
|
||||||
|
|
||||||
// Patch fetch again
|
// Patch fetch again
|
||||||
patch_builtin('fetch', async () => 'second')
|
app_window_patches = {fetch: async () => 'second'}
|
||||||
|
|
||||||
const cache_discarded = await command_input_async(initial, `
|
const cache_discarded = await input_async(initial, `
|
||||||
console.log(await fetch('url', {method: 'POST'}))
|
console.log(await fetch('url', {method: 'POST'}))
|
||||||
`, 0)
|
`, 0, {app_window_patches})
|
||||||
assert_equal(cache_discarded.logs.logs[0].args[0], 'second')
|
assert_equal(cache_discarded.logs.logs[0].args[0], 'second')
|
||||||
|
|
||||||
// Remove patch
|
|
||||||
patch_builtin('fetch', null)
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
test('record io fetch rejects', async () => {
|
test('record io fetch rejects', async () => {
|
||||||
// Patch fetch
|
// Patch fetch
|
||||||
patch_builtin('fetch', () => Promise.reject('fail'))
|
let app_window_patches
|
||||||
|
app_window_patches = {fetch: () => globalThis.app_window.Promise.reject('fail')}
|
||||||
|
|
||||||
const initial = await test_initial_state_async(`
|
const initial = await test_initial_state_async(`
|
||||||
await fetch('url', {method: 'GET'})
|
await fetch('url', {method: 'GET'})
|
||||||
`)
|
`, undefined, {app_window_patches})
|
||||||
assert_equal(root_calltree_node(initial).error, 'fail')
|
assert_equal(root_calltree_node(initial).error, 'fail')
|
||||||
|
|
||||||
// Patch fetch again
|
// Patch fetch again
|
||||||
patch_builtin('fetch', () => async () => 'result')
|
app_window_patches = {fetch: () => async () => 'result'}
|
||||||
|
|
||||||
const with_cache = await command_input_async(initial, `
|
const with_cache = await input_async(initial, `
|
||||||
await fetch('url', {method: 'GET'})
|
await fetch('url', {method: 'GET'})
|
||||||
`, 0)
|
`, 0, {app_window_patches})
|
||||||
assert_equal(root_calltree_node(initial).error, 'fail')
|
assert_equal(root_calltree_node(initial).error, 'fail')
|
||||||
|
|
||||||
// Remove patch
|
|
||||||
patch_builtin('fetch', null)
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
test('record io preserve promise resolution order', async () => {
|
test('record io preserve promise resolution order', async () => {
|
||||||
// Generate fetch function which calls get resolved in reverse order
|
// Generate fetch function which calls get resolved in reverse order
|
||||||
const {fetch, resolve} = new Function(`
|
const calls = []
|
||||||
const calls = []
|
function fetch(...args) {
|
||||||
return {
|
let resolver
|
||||||
fetch(...args) {
|
const promise = new (globalThis.app_window.Promise)(r => {resolver = r})
|
||||||
let resolver
|
calls.push({resolver, promise, args})
|
||||||
const promise = new Promise(r => resolver = r)
|
return promise
|
||||||
calls.push({resolver, promise, args})
|
}
|
||||||
return promise
|
|
||||||
},
|
|
||||||
|
|
||||||
resolve() {
|
function resolve() {
|
||||||
[...calls].reverse().forEach(call => call.resolver(...call.args))
|
[...calls].reverse().forEach(call => call.resolver(...call.args))
|
||||||
},
|
}
|
||||||
}
|
|
||||||
`)()
|
let app_window_patches
|
||||||
|
|
||||||
// Patch fetch
|
// Patch fetch
|
||||||
patch_builtin('fetch', fetch)
|
app_window_patches = {fetch}
|
||||||
|
|
||||||
const code = `
|
const code = `
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -4096,7 +4056,7 @@ const y = x()`
|
|||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
const initial_promise = test_initial_state_async(code)
|
const initial_promise = test_initial_state_async(code, undefined, {app_window_patches})
|
||||||
|
|
||||||
resolve()
|
resolve()
|
||||||
|
|
||||||
@@ -4106,56 +4066,54 @@ const y = x()`
|
|||||||
assert_equal(initial.logs.logs.map(l => l.args[0]), [3,2,1])
|
assert_equal(initial.logs.logs.map(l => l.args[0]), [3,2,1])
|
||||||
|
|
||||||
// Break fetch to ensure it is not get called anymore
|
// Break fetch to ensure it is not get called anymore
|
||||||
patch_builtin('fetch', () => {throw 'broken'})
|
app_window_patches = {fetch: () => {throw 'broken'}}
|
||||||
|
|
||||||
const with_cache = await command_input_async(
|
const with_cache = await input_async(
|
||||||
initial,
|
initial,
|
||||||
code,
|
code,
|
||||||
0
|
0,
|
||||||
|
{app_window_patches}
|
||||||
)
|
)
|
||||||
|
|
||||||
// cached calls to fetch should be resolved in the same (reverse) order as
|
// cached calls to fetch should be resolved in the same (reverse) order as
|
||||||
// on the first run, so first call wins
|
// on the first run, so first call wins
|
||||||
assert_equal(with_cache.logs.logs.map(l => l.args[0]), [3,2,1])
|
assert_equal(with_cache.logs.logs.map(l => l.args[0]), [3,2,1])
|
||||||
|
|
||||||
// Remove patch
|
|
||||||
patch_builtin('fetch', null)
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
test('record io setTimeout', async () => {
|
test('record io setTimeout', async () => {
|
||||||
|
let app_window_patches
|
||||||
// Patch fetch to return result in 10ms
|
// Patch fetch to return result in 10ms
|
||||||
patch_builtin(
|
app_window_patches = {
|
||||||
'fetch',
|
fetch: () => new (globalThis.app_window.Promise)(resolve => setTimeout(resolve, 10))
|
||||||
() => new Promise(resolve => original_setTimeout(resolve, 10))
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const code = `
|
const code = `
|
||||||
setTimeout(() => console.log('timeout'), 0)
|
setTimeout(() => console.log('timeout'), 0)
|
||||||
await fetch().then(() => console.log('fetch'))
|
await fetch().then(() => console.log('fetch'))
|
||||||
`
|
`
|
||||||
|
|
||||||
const i = await test_initial_state_async(code)
|
const i = await test_initial_state_async(code, undefined, {app_window_patches})
|
||||||
|
|
||||||
// First executed setTimeout, then fetch
|
// First executed setTimeout, then fetch
|
||||||
assert_equal(i.logs.logs.map(l => l.args[0]), ['timeout', 'fetch'])
|
assert_equal(i.logs.logs.map(l => l.args[0]), ['timeout', 'fetch'])
|
||||||
|
|
||||||
// Break fetch to ensure it would not be called
|
// Break fetch to ensure it would not be called
|
||||||
patch_builtin('fetch', async () => {throw 'break'})
|
app_window_patches = {
|
||||||
|
fetch: async () => {throw 'break'}
|
||||||
|
}
|
||||||
|
|
||||||
const with_cache = await command_input_async(i, code, 0)
|
const with_cache = await input_async(i, code, 0, {app_window_patches})
|
||||||
|
|
||||||
// Cache must preserve resolution order
|
// Cache must preserve resolution order
|
||||||
assert_equal(with_cache.logs.logs.map(l => l.args[0]), ['timeout', 'fetch'])
|
assert_equal(with_cache.logs.logs.map(l => l.args[0]), ['timeout', 'fetch'])
|
||||||
|
|
||||||
patch_builtin('fetch', null)
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
test('record io clear io trace', async () => {
|
test('record io clear io trace', async () => {
|
||||||
const s1 = test_initial_state(`Math.random()`)
|
const s1 = test_initial_state(`Math.random()`)
|
||||||
const rnd = s1.value_explorer.result.value
|
const rnd = s1.value_explorer.result.value
|
||||||
const s2 = COMMANDS.input(s1, `Math.random() + 1`, 0).state
|
const s2 = input(s1, `Math.random() + 1`, 0).state
|
||||||
assert_equal(s2.value_explorer.result.value, rnd + 1)
|
assert_equal(s2.value_explorer.result.value, rnd + 1)
|
||||||
const cleared = COMMANDS.clear_io_trace(s2)
|
const cleared = input(COMMANDS.clear_io_trace(s2), `Math.random() + 1`).state
|
||||||
assert_equal(
|
assert_equal(
|
||||||
cleared.value_explorer.result.value == rnd + 1,
|
cleared.value_explorer.result.value == rnd + 1,
|
||||||
false
|
false
|
||||||
@@ -4185,8 +4143,8 @@ const y = x()`
|
|||||||
const rnd = i.active_calltree_node.children[0].value
|
const rnd = i.active_calltree_node.children[0].value
|
||||||
|
|
||||||
// Run two versions of code in parallel
|
// Run two versions of code in parallel
|
||||||
const next = COMMANDS.input(i, `await Promise.resolve()`, 0)
|
const next = input(i, `await Promise.resolve()`, 0)
|
||||||
const next2 = COMMANDS.input(i, `Math.random(1)`, 0).state
|
const next2 = input(i, `Math.random(1)`, 0).state
|
||||||
const next_rnd = i.active_calltree_node.children[0].value
|
const next_rnd = i.active_calltree_node.children[0].value
|
||||||
assert_equal(rnd, next_rnd)
|
assert_equal(rnd, next_rnd)
|
||||||
}),
|
}),
|
||||||
@@ -4211,10 +4169,10 @@ const y = x()`
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
test('record io hangs bug', async () => {
|
test('record io hangs bug', async () => {
|
||||||
patch_builtin(
|
let app_window_patches
|
||||||
'fetch',
|
app_window_patches = {
|
||||||
() => new Promise(resolve => original_setTimeout(resolve, 0))
|
fetch: () => new (globalThis.app_window.Promise)(resolve => setTimeout(resolve, 0))
|
||||||
)
|
}
|
||||||
|
|
||||||
const code = `
|
const code = `
|
||||||
const p = fetch('')
|
const p = fetch('')
|
||||||
@@ -4222,22 +4180,20 @@ const y = x()`
|
|||||||
await p
|
await p
|
||||||
`
|
`
|
||||||
|
|
||||||
const i = await test_initial_state_async(code)
|
const i = await test_initial_state_async(code, undefined, {app_window_patches})
|
||||||
|
|
||||||
assert_equal(i.io_trace.length, 3)
|
assert_equal(i.io_trace.length, 3)
|
||||||
|
|
||||||
const next_code = `await fetch('')`
|
const next_code = `await fetch('')`
|
||||||
|
|
||||||
const state = await command_input_async(i, next_code, 0)
|
const state = await input_async(i, next_code, 0, {app_window_patches})
|
||||||
assert_equal(state.io_trace.length, 2)
|
assert_equal(state.io_trace.length, 2)
|
||||||
|
|
||||||
patch_builtin('fetch', null)
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
test('record io logs recorded twice bug', () => {
|
test('record io logs recorded twice bug', () => {
|
||||||
const code = `Math.random()`
|
const code = `Math.random()`
|
||||||
const i = test_initial_state(code)
|
const i = test_initial_state(code)
|
||||||
const second = COMMANDS.input(
|
const second = input(
|
||||||
i,
|
i,
|
||||||
`console.log(1); Math.random(); Math.random()`,
|
`console.log(1); Math.random(); Math.random()`,
|
||||||
0
|
0
|
||||||
@@ -5401,4 +5357,19 @@ const y = x()`
|
|||||||
assert_value_explorer(i, expected)
|
assert_value_explorer(i, expected)
|
||||||
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
test('leporello storage API', () => {
|
||||||
|
const i = test_initial_state(`
|
||||||
|
const value = leporello.storage.get('value')
|
||||||
|
if(value == null) {
|
||||||
|
leporello.storage.set('value', 1)
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
const with_storage = input(i, 'leporello.storage.get("value")', 0).state
|
||||||
|
assert_value_explorer(with_storage, 1)
|
||||||
|
const with_cleared_storage = run_code(
|
||||||
|
COMMANDS.open_app_window(with_storage)
|
||||||
|
)
|
||||||
|
assert_value_explorer(with_cleared_storage, undefined)
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|||||||
165
test/utils.js
165
test/utils.js
@@ -3,60 +3,22 @@ import {parse, print_debug_node, load_modules} from '../src/parse_js.js'
|
|||||||
import {active_frame, pp_calltree, version_number_symbol} from '../src/calltree.js'
|
import {active_frame, pp_calltree, version_number_symbol} from '../src/calltree.js'
|
||||||
import {COMMANDS} from '../src/cmd.js'
|
import {COMMANDS} from '../src/cmd.js'
|
||||||
|
|
||||||
|
// external
|
||||||
|
import {create_app_window} from './run_utils.js'
|
||||||
|
|
||||||
// external
|
// external
|
||||||
import {with_version_number} from '../src/runtime/runtime.js'
|
import {with_version_number} from '../src/runtime/runtime.js'
|
||||||
|
|
||||||
Object.assign(globalThis,
|
Object.assign(globalThis,
|
||||||
{
|
{
|
||||||
// for convenince, to type just `log` instead of `console.log`
|
// for convenience, to type just `log` instead of `console.log`
|
||||||
log: console.log,
|
log: console.log,
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const patch_builtin = new Function(`
|
|
||||||
if(globalThis.process != null ) {
|
|
||||||
globalThis.app_window = globalThis
|
|
||||||
} else {
|
|
||||||
const iframe = globalThis.document.createElement('iframe')
|
|
||||||
globalThis.document.body.appendChild(iframe)
|
|
||||||
globalThis.app_window = iframe.contentWindow
|
|
||||||
}
|
|
||||||
let originals = globalThis.app_window.__builtins_originals
|
|
||||||
let patched = globalThis.app_window.__builtins_patched
|
|
||||||
if(originals == null) {
|
|
||||||
globalThis.app_window.__original_setTimeout = globalThis.setTimeout
|
|
||||||
// This code can execute twice when tests are run in self-hosted mode.
|
|
||||||
// Ensure that patches will be applied only once
|
|
||||||
originals = globalThis.app_window.__builtins_originals = {}
|
|
||||||
patched = globalThis.app_window.__builtins_patched = {}
|
|
||||||
|
|
||||||
const patch = (obj, name) => {
|
|
||||||
originals[name] = obj[name]
|
|
||||||
obj[name] = (...args) => {
|
|
||||||
return patched[name] == null
|
|
||||||
? originals[name].apply(null, args)
|
|
||||||
: patched[name].apply(null, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute some builtin functions: fetch, setTimeout, Math.random to be
|
|
||||||
// able to patch them in tests
|
|
||||||
patch(globalThis.app_window, 'fetch')
|
|
||||||
patch(globalThis.app_window, 'setTimeout')
|
|
||||||
patch(globalThis.app_window.Math, 'random')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (name, fn) => {
|
|
||||||
patched[name] = fn
|
|
||||||
}
|
|
||||||
`)()
|
|
||||||
|
|
||||||
export const original_setTimeout = globalThis.app_window.__original_setTimeout
|
|
||||||
|
|
||||||
export const do_parse = code => parse(
|
export const do_parse = code => parse(
|
||||||
code,
|
code,
|
||||||
new Set(Object.getOwnPropertyNames(globalThis.app_window))
|
new Set(Object.getOwnPropertyNames(globalThis))
|
||||||
)
|
)
|
||||||
|
|
||||||
export const parse_modules = (entry, modules) =>
|
export const parse_modules = (entry, modules) =>
|
||||||
@@ -102,18 +64,48 @@ export const assert_code_error_async = async (codestring, error) => {
|
|||||||
assert_equal(result.error, error)
|
assert_equal(result.error, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patch_app_window(app_window_patches) {
|
||||||
|
Object.entries(app_window_patches).forEach(([path, value]) => {
|
||||||
|
let obj = globalThis.app_window
|
||||||
|
const path_arr = path.split('.')
|
||||||
|
path_arr.forEach((el, i) => {
|
||||||
|
if(i == path_arr.length - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obj = obj[el]
|
||||||
|
})
|
||||||
|
const prop = path_arr.at(-1)
|
||||||
|
obj[prop] = value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const run_code = (state, app_window_patches) => {
|
||||||
|
globalThis.app_window = create_app_window()
|
||||||
|
if(app_window_patches != null) {
|
||||||
|
patch_app_window(app_window_patches)
|
||||||
|
}
|
||||||
|
return COMMANDS.run_code(
|
||||||
|
state,
|
||||||
|
state.globals ?? new Set(Object.getOwnPropertyNames(globalThis.app_window))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const test_initial_state = (code, cursor_pos, options = {}) => {
|
export const test_initial_state = (code, cursor_pos, options = {}) => {
|
||||||
if(cursor_pos < 0) {
|
if(cursor_pos < 0) {
|
||||||
throw new Error('illegal cursor_pos')
|
throw new Error('illegal cursor_pos')
|
||||||
}
|
}
|
||||||
|
if(typeof(options) != 'object') {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
const {
|
const {
|
||||||
//entrypoint = '',
|
|
||||||
current_module,
|
current_module,
|
||||||
project_dir,
|
project_dir,
|
||||||
on_deferred_call,
|
on_deferred_call,
|
||||||
|
app_window_patches,
|
||||||
} = options
|
} = options
|
||||||
const entrypoint = options.entrypoint ?? ''
|
const entrypoint = options.entrypoint ?? ''
|
||||||
return COMMANDS.open_app_window(
|
|
||||||
|
return run_code(
|
||||||
COMMANDS.get_initial_state(
|
COMMANDS.get_initial_state(
|
||||||
{
|
{
|
||||||
files: typeof(code) == 'object' ? code : { '' : code},
|
files: typeof(code) == 'object' ? code : { '' : code},
|
||||||
@@ -126,7 +118,7 @@ export const test_initial_state = (code, cursor_pos, options = {}) => {
|
|||||||
},
|
},
|
||||||
cursor_pos
|
cursor_pos
|
||||||
),
|
),
|
||||||
new Set(Object.getOwnPropertyNames(globalThis.app_window))
|
app_window_patches,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,8 +133,19 @@ export const test_initial_state_async = async (code, ...args) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const command_input_async = async (...args) => {
|
export const input = (s, code, index, options = {}) => {
|
||||||
const after_input = COMMANDS.input(...args).state
|
if(typeof(options) != 'object') {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
const {state, effects} = COMMANDS.input(s, code, index)
|
||||||
|
return {
|
||||||
|
state: run_code(state, options.app_window_patches),
|
||||||
|
effects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input_async = async (...args) => {
|
||||||
|
const after_input = input(...args).state
|
||||||
const result = await after_input.eval_modules_state.promise
|
const result = await after_input.eval_modules_state.promise
|
||||||
return COMMANDS.eval_modules_finished(
|
return COMMANDS.eval_modules_finished(
|
||||||
after_input,
|
after_input,
|
||||||
@@ -239,3 +242,69 @@ export const test = (message, test, only = false) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const test_only = (message, t) => test(message, t, true)
|
export const test_only = (message, t) => test(message, t, true)
|
||||||
|
|
||||||
|
// Create `run` function like this because we do not want its internals to be
|
||||||
|
// present in calltree
|
||||||
|
export const run = new Function('create_app_window', `
|
||||||
|
return function run(tests) {
|
||||||
|
// create app window for simple tests, that do not use 'test_initial_state'
|
||||||
|
globalThis.app_window = create_app_window()
|
||||||
|
|
||||||
|
// Runs test, return failure or null if not failed
|
||||||
|
const run_test = t => {
|
||||||
|
return Promise.resolve().then(t.test)
|
||||||
|
.then(() => null)
|
||||||
|
.catch(e => {
|
||||||
|
if(globalThis.process != null) {
|
||||||
|
// In node.js runner, fail fast
|
||||||
|
console.error('Failed: ' + t.message)
|
||||||
|
throw e
|
||||||
|
} else {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not run in node, then dont apply filter
|
||||||
|
const filter = globalThis.process && globalThis.process.argv[2]
|
||||||
|
|
||||||
|
if(filter == null) {
|
||||||
|
|
||||||
|
const only = tests.find(t => t.only)
|
||||||
|
const tests_to_run = only == null ? tests : [only]
|
||||||
|
|
||||||
|
// Exec each test. After all tests are done, we rethrow first error if
|
||||||
|
// any. So we will mark root calltree node if one of tests failed
|
||||||
|
return tests_to_run.reduce(
|
||||||
|
(failureP, t) =>
|
||||||
|
Promise.resolve(failureP).then(failure =>
|
||||||
|
run_test(t).then(next_failure => failure ?? next_failure)
|
||||||
|
)
|
||||||
|
,
|
||||||
|
null
|
||||||
|
).then(failure => {
|
||||||
|
|
||||||
|
if(failure != null) {
|
||||||
|
throw failure
|
||||||
|
} else {
|
||||||
|
if(globalThis.process != null) {
|
||||||
|
console.log('Ok')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const test = tests.find(t => t.message.includes(filter))
|
||||||
|
if(test == null) {
|
||||||
|
throw new Error('test not found')
|
||||||
|
} else {
|
||||||
|
return run_test(test).then(() => {
|
||||||
|
if(globalThis.process != null) {
|
||||||
|
console.log('Ok')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)(create_app_window)
|
||||||
|
|||||||
Reference in New Issue
Block a user