diff --git a/apps/repl/app/snippets.ts b/apps/repl/app/snippets.ts index 6a7708bd8..0c092592c 100644 --- a/apps/repl/app/snippets.ts +++ b/apps/repl/app/snippets.ts @@ -40,6 +40,8 @@ export const ALL = [ export const NAMES = ALL.map((demo) => demo.label); +export const LOADED = new Set([DEFAULT_SNIPPET]); + export async function getFromLabel(label: string): Promise { let entry = ALL.find((entry) => entry.label === label); @@ -53,5 +55,7 @@ export async function getFromLabel(label: string): Promise { let response = await fetch(path); let text = await response.text(); + LOADED.add(text); + return text; } diff --git a/apps/repl/app/utils/editor-text.ts b/apps/repl/app/utils/editor-text.ts index db9e2d313..b5a8fcda9 100644 --- a/apps/repl/app/utils/editor-text.ts +++ b/apps/repl/app/utils/editor-text.ts @@ -133,22 +133,8 @@ export class FileURIComponent { #timeout?: ReturnType; #queuedFn?: () => void; - /** - * Debounce so we are kinder on the CPU - */ queue = (rawText: string, format: Format) => { - if (this.#timeout) clearTimeout(this.#timeout); - - this.#queuedFn = () => { - if (isDestroyed(this) || isDestroying(this)) return; - - this.set(rawText, format); - this.#queuedFn = undefined; - queueTokens.forEach((token) => queueWaiter.endAsync(token)); - }; - - this.#timeout = setTimeout(this.#queuedFn, DEBOUNCE_MS); - queueTokens.push(queueWaiter.beginAsync()); + this.set(rawText, format); }; #flush = () => { @@ -172,64 +158,63 @@ export class FileURIComponent { return base ?? window.location.toString(); }; - #lastQPs: URLSearchParams | undefined; + #updateWaiter: unknown; + #frame?: number; + #qps: URLSearchParams | undefined; #updateQPs = async (rawText: string, format: Format) => { - let isFast = new Date().getTime() - this.#rapidCallTime < 100; - - if (!isFast) { - this.#rapidCallQPs = []; - this.#rapidCallCount = 0; - this.#rapidCallTime = -Infinity; - } + if (this.#frame) cancelAnimationFrame(this.#frame); + if (!this.#updateWaiter) this.#updateWaiter = queueWaiter.beginAsync(); let encoded = compressToEncodedURIComponent(rawText); let qps = new URLSearchParams(location.search); - if (isFast && this.#rapidCallCount > 1) { - let isIrrelevant = - this.#lastQPs && - [...qps.entries()].every(([key, value]) => { - return this.#lastQPs?.get(key) === value; - }); + qps.set('c', encoded); + qps.delete('t'); + qps.set('format', formatFrom(format)); - if (isIrrelevant) return; + this.#qps = { + ...this.#qps, + ...qps, + }; - console.debug(this.#rapidCallQPs); + this.#frame = requestAnimationFrame(async () => { + if (isDestroyed(this) || isDestroying(this)) { + queueWaiter.endAsync(this.#updateWaiter); + this.#updateWaiter = null; - let error = new Error('Too many rapid query param changes'); + return; + } - console.debug(error.stack); - throw error; - } + /** + * Debounce so we are kinder on the CPU + */ + await new Promise((resolve) => setTimeout(resolve, DEBOUNCE_MS)); - this.#rapidCallTime = new Date().getTime(); - this.#rapidCallCount++; - this.#rapidCallQPs.push(qps); + if (isDestroyed(this) || isDestroying(this)) { + queueWaiter.endAsync(this.#updateWaiter); + this.#updateWaiter = null; - qps.set('c', encoded); - qps.delete('t'); - qps.set('format', formatFrom(format)); + return; + } - this.#lastQPs = qps; + queueWaiter.endAsync(this.#updateWaiter); + this.#updateWaiter = null; - // On initial load, if we call #updateQPs, - // we may not have a currentURL, because the first transition has yet to complete - let base = this.router.currentURL?.split('?')[0]; + // On initial load, if we call #updateQPs, + // we may not have a currentURL, because the first transition has yet to complete + let base = this.router.currentURL?.split('?')[0]; - if (macroCondition(isTesting())) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - base ??= (this.router as any) /* private API? */?.location?.path; - } else { - base ??= window.location.pathname; - } + if (macroCondition(isTesting())) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + base ??= (this.router as any) /* private API? */?.location?.path; + } else { + base ??= window.location.pathname; + } - let next = `${base}?${qps}`; + let next = `${base}?${qps}`; - this.router.replaceWith(next); - this.#text = rawText; + this.router.replaceWith(next); + this.#text = rawText; + }); }; - - #rapidCallTime = -Infinity; - #rapidCallCount = 0; - #rapidCallQPs: unknown[] = []; } diff --git a/apps/repl/tests/application/-page/index.ts b/apps/repl/tests/application/-page/index.ts index 86b0f26f7..8fd537807 100644 --- a/apps/repl/tests/application/-page/index.ts +++ b/apps/repl/tests/application/-page/index.ts @@ -1,4 +1,8 @@ +import { assert } from '@ember/debug'; +import { currentURL, settled, visit } from '@ember/test-helpers'; + import { PageObject } from 'fractal-page-object'; +import { decompressFromEncodedURIComponent } from 'lz-string'; import { s } from './-helpers'; import { DemoSelect } from './demo-select'; @@ -15,4 +19,64 @@ export class Page extends PageObject { selectDemo(text: string) { return this.demo.select(text); } + + async expectRedirectToContent( + to: string, + { c, t, format }: { t?: string; c?: string; format?: string } = {} + ) { + let sawExpectedError = false; + + try { + await visit(to); + } catch (e) { + assert('Expected error to be an object', typeof e === 'object' && e !== null); + assert( + 'Expected error to have a message property', + 'message' in e && typeof e.message === 'string' + ); + + let lines = e.message.split('\n'); + let first = lines[0]; + + assert( + `The only expected error is a TransitionAborted. Received: ${first}`, + first === 'TransitionAborted' + ); + sawExpectedError = true; + } + + assert(`Expected to see a TransitionAborted error, but it did not occur.`, sawExpectedError); + + // Allow time for transitions to settle + await settled(); + + let url = currentURL(); + + assert(`Expected an URL, got ${url}`, url); + + let [, search] = url.split('?'); + let query = new URLSearchParams(search); + + if (format) { + let f = query.get('format'); + + assert(`Expected format, ${format}, but got ${f}`, f === format); + } + + if (c) { + let lzString = query.get('c'); + + assert(`Missing c query param`, lzString); + + let value = decompressFromEncodedURIComponent(lzString); + + assert(`QP's c did not match expected text`, c === value); + } + + if (t) { + let text = query.get('t'); + + assert(`QP's t did not match expected text`, text === t); + } + } } diff --git a/apps/repl/tests/application/editor-format-test.gts b/apps/repl/tests/application/editor-format-test.gts index 5e944383c..539237bc1 100644 --- a/apps/repl/tests/application/editor-format-test.gts +++ b/apps/repl/tests/application/editor-format-test.gts @@ -28,14 +28,19 @@ module('Editor > Format', function (hooks) { }); test('defaults to glimdown', async function (assert) { - await visit('/edit'); + await page.expectRedirectToContent('/edit', { + format: 'glimdown', + }); + await page.editor.load(); assert.strictEqual(page.editor.format, 'glimdown'); }); test('when choosing a format, text is required -- otherwise glimdown is chosen', async function (assert) { - await visit('/edit?format=gjs'); + await page.expectRedirectToContent('/edit?format=gjs', { + format: 'glimdown', + }); await page.editor.load(); assert.strictEqual(page.editor.format, 'glimdown'); @@ -50,7 +55,9 @@ module('Editor > Format', function (hooks) { }); test('can start with glimdown, and change to gjs', async function (assert) { - await visit('/edit'); + await page.expectRedirectToContent(`/edit`, { + format: 'glimdown', + }); await page.editor.load(); assert.strictEqual(page.editor.format, 'glimdown'); @@ -62,7 +69,9 @@ module('Editor > Format', function (hooks) { }); test('can start with glimdown, and is able to change formats via the URL', async function (assert) { - await visit('/edit'); + await page.expectRedirectToContent(`/edit`, { + format: 'glimdown', + }); await page.editor.load(); assert.strictEqual(page.editor.format, 'glimdown'); diff --git a/apps/repl/tests/application/output-demos-test.gts b/apps/repl/tests/application/output-demos-test.gts index 4b72e42df..b196910f8 100644 --- a/apps/repl/tests/application/output-demos-test.gts +++ b/apps/repl/tests/application/output-demos-test.gts @@ -1,5 +1,5 @@ import { assert as debugAssert } from '@ember/debug'; -import { settled, visit } from '@ember/test-helpers'; +import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; @@ -41,7 +41,7 @@ module('Output > Demos', function (hooks) { test(demo.label, async function (assert) { this.owner.register('template:edit', Route()); - await visit('/edit'); + await page.expectRedirectToContent('/edit'); await page.selectDemo(demo.label); let { queryParams = {} } = getService('router').currentRoute ?? {}; @@ -80,7 +80,7 @@ module('Output > Demos', function (hooks) { ) ); - await visit('/edit'); + await page.expectRedirectToContent('/edit'); debugAssert(`setParentFrame did not get set`, setParentFrame); debugAssert(`makeComponent did not get set`, makeComponent);