From e3a3a07a90ad4c73fc8c074163725c1134a40c4c Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 5 Feb 2025 15:47:03 -0500 Subject: [PATCH 01/21] improve(laboratory): validate editor content with TypeScript --- .../app/src/lib/preflight/graphiql-plugin.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx index 30fb4f595a..a08108cea1 100644 --- a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx @@ -104,6 +104,12 @@ const monacoProps = { defaultLanguage: 'javascript', options: { ...sharedMonacoProps.options, + quickSuggestions: true, + suggestOnTriggerCharacters: true, + acceptSuggestionOnEnter: 'on', + tabCompletion: 'on', + folding: true, + foldingStrategy: 'indentation', }, }, } satisfies Record<'script' | 'env', ComponentPropsWithoutRef>; @@ -639,6 +645,29 @@ function PreflightModal({ }, []); const handleMonacoEditorBeforeMount = useCallback((monaco: Monaco) => { + // Configure JavaScript defaults for TypeScript validation + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false, + noSuggestionDiagnostics: false, + diagnosticCodesToIgnore: [], // Can specify codes to ignore + }); + + // Enable modern JavaScript features and strict checks + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ESNext, + allowNonTsExtensions: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + module: monaco.languages.typescript.ModuleKind.ESNext, + noEmit: true, + lib: ['es2021', 'dom'], + strict: true, + noUnusedLocals: true, + noUnusedParameters: true, + noImplicitReturns: true, + noFallthroughCasesInSwitch: true, + }); + // Add custom typings for globalThis monaco.languages.typescript.javascriptDefaults.addExtraLib( ` From 6fdd227da538900f4040d4f42eedf2d056180f71 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 10:34:31 -0500 Subject: [PATCH 02/21] group laboratory e2e tests --- cypress.config.ts | 8 ++ cypress/e2e/laboratory/_cy.ts | 87 ++++++++++++ .../collections.cy.ts} | 28 ++-- .../preflight.cy.ts} | 133 ++++++++---------- .../tabs.cy.ts} | 22 +-- cypress/support/dedent.ts | 52 +++++++ cypress/support/e2e.ts | 1 + cypress/support/monaco.ts | 18 +++ cypress/support/testkit.ts | 99 ------------- package.json | 2 + pnpm-lock.yaml | 13 ++ tsconfig.json | 2 +- 12 files changed, 263 insertions(+), 202 deletions(-) create mode 100644 cypress/e2e/laboratory/_cy.ts rename cypress/e2e/{laboratory-collections.cy.ts => laboratory/collections.cy.ts} (91%) rename cypress/e2e/{laboratory-preflight.cy.ts => laboratory/preflight.cy.ts} (80%) rename cypress/e2e/{laboratory-tabs.cy.ts => laboratory/tabs.cy.ts} (56%) create mode 100644 cypress/support/dedent.ts create mode 100644 cypress/support/monaco.ts diff --git a/cypress.config.ts b/cypress.config.ts index 4bd27e952a..b12214e49c 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -8,6 +8,14 @@ if (!process.env.RUN_AGAINST_LOCAL_SERVICES) { dotenv.config({ path: import.meta.dirname + '/integration-tests/.env' }); } +if (process.env.RUN_AGAINST_LOCAL_SERVICES === '1') { + process.env.SUPERTOKENS_API_KEY = process.env.SUPERTOKENS_API_KEY ?? 'bubatzbieber6942096420'; + process.env.SUPERTOKENS_CONNECTION_URI = + process.env.SUPERTOKENS_CONNECTION_URI ?? 'http://localhost:3567'; + // It seems that this has to be set in the environment that the cypress cli is executed from. + // process.env.CYPRESS_BASE_URL = process.env.CYPRESS_BASE_URL ?? 'http://localhost:3000'; +} + const isCI = Boolean(process.env.CI); export const seed = initSeed(); diff --git a/cypress/e2e/laboratory/_cy.ts b/cypress/e2e/laboratory/_cy.ts new file mode 100644 index 0000000000..f19d690ab3 --- /dev/null +++ b/cypress/e2e/laboratory/_cy.ts @@ -0,0 +1,87 @@ +import { setMonacoEditorContents } from '../../support/monaco'; + +export namespace cyLaboratory { + /** + * Updates the value of the graphiql editor + */ + export function updateEditorValue(value: string) { + cy.get('.graphiql-query-editor .cm-s-graphiql').then($editor => { + const editor = ($editor[0] as any).CodeMirror; // Access the CodeMirror instance + editor.setValue(value); + }); + } + + /** + * Returns the value of the graphiql editor as Chainable + */ + export function getEditorValue() { + return cy.get('.graphiql-query-editor .cm-s-graphiql').then($editor => { + const editor = ($editor[0] as any).CodeMirror; // Access the CodeMirror instance + return editor.getValue(); + }); + } + + /** + * Opens a new tab + */ + export function openNewTab() { + cy.get('button[aria-label="New tab"]').click(); + // tab's title should be "untitled" as it's a default name + cy.contains('button[aria-controls="graphiql-session"]', 'untitled').should('exist'); + } + + /** + * Asserts that the tab with the given name is active + */ + export function assertActiveTab(name: string) { + cy.contains('li.graphiql-tab-active > button[aria-controls="graphiql-session"]', name).should( + 'exist', + ); + } + + /** + * Closes the active tab + */ + export function closeActiveTab() { + cy.get('li.graphiql-tab-active > button.graphiql-tab-close').click(); + } + + /** + * Closes all tabs until one is left + */ + export function closeTabsUntilOneLeft() { + cy.get('li.graphiql-tab').then($tabs => { + if ($tabs.length > 1) { + closeActiveTab(); + // Recurse until there's only one tab left + return closeTabsUntilOneLeft(); + } + }); + } + + export namespace preflight { + export const selectors = { + buttonGraphiQLPreflight: '[aria-label*="Preflight Script"]', + buttonModalCy: 'preflight-modal-button', + buttonToggleCy: 'toggle-preflight', + buttonHeaders: '[data-name="headers"]', + headersEditor: { + textArea: '.graphiql-editor-tool .graphiql-editor:last-child textarea', + }, + graphiql: { + buttonExecute: '.graphiql-execute-button', + }, + + modal: { + buttonSubmitCy: 'preflight-modal-submit', + editorCy: 'preflight-editor', + }, + }; + /** + * Sets the content of the preflight editor + */ + export function setEditorContent(value: string) { + setMonacoEditorContents(selectors.modal.editorCy, value); + } + } +} diff --git a/cypress/e2e/laboratory-collections.cy.ts b/cypress/e2e/laboratory/collections.cy.ts similarity index 91% rename from cypress/e2e/laboratory-collections.cy.ts rename to cypress/e2e/laboratory/collections.cy.ts index 265ef18d4e..086aa410b5 100644 --- a/cypress/e2e/laboratory-collections.cy.ts +++ b/cypress/e2e/laboratory/collections.cy.ts @@ -1,4 +1,4 @@ -import { laboratory } from '../support/testkit'; +import { cyLaboratory } from './_cy'; beforeEach(() => { cy.clearAllLocalStorage().then(() => { @@ -16,7 +16,7 @@ beforeEach(() => { .first() .click(); cy.get('[aria-label="Show Operation Collections"]').click(); - laboratory.closeTabsUntilOneLeft(); + cyLaboratory.closeTabsUntilOneLeft(); }); }); }); @@ -90,7 +90,7 @@ describe('Laboratory > Collections', () => { name: 'collection-1', description: 'Description 1', }); - laboratory.updateEditorValue(`query op1 { test }`); + cyLaboratory.updateEditorValue(`query op1 { test }`); collections.saveCurrentOperationAs({ name: 'operation-1', collectionName: 'collection-1', @@ -103,7 +103,7 @@ describe('Laboratory > Collections', () => { name: 'collection-1', description: 'Description 1', }); - laboratory.updateEditorValue(`query op1 { test }`); + cyLaboratory.updateEditorValue(`query op1 { test }`); collections.saveCurrentOperationAs({ name: 'operation-1', collectionName: 'collection-1', @@ -127,7 +127,7 @@ describe('Laboratory > Collections', () => { name: 'collection-1', description: 'Description 1', }); - laboratory.updateEditorValue(`query op1 { test }`); + cyLaboratory.updateEditorValue(`query op1 { test }`); collections.saveCurrentOperationAs({ name: 'operation-1', collectionName: 'collection-1', @@ -151,7 +151,7 @@ describe('Laboratory > Collections', () => { name: 'collection-1', description: 'Description 1', }); - laboratory.updateEditorValue(`query op1 { test }`); + cyLaboratory.updateEditorValue(`query op1 { test }`); collections.saveCurrentOperationAs({ name: 'operation-1', collectionName: 'collection-1', @@ -173,14 +173,14 @@ describe('Laboratory > Collections', () => { name: 'collection-1', description: 'Description 1', }); - laboratory.updateEditorValue(`query op1 { test }`); + cyLaboratory.updateEditorValue(`query op1 { test }`); collections.saveCurrentOperationAs({ name: 'operation-1', collectionName: 'collection-1', }); - laboratory.openNewTab(); - laboratory.updateEditorValue(`query op2 { test }`); + cyLaboratory.openNewTab(); + cyLaboratory.updateEditorValue(`query op2 { test }`); collections.saveCurrentOperationAs({ name: 'operation-2', collectionName: 'collection-1', @@ -206,14 +206,14 @@ describe('Laboratory > Collections', () => { description: 'Description 2', }); collections.clickCollectionButton('collection-1'); - laboratory.updateEditorValue(`query op1 { test }`); + cyLaboratory.updateEditorValue(`query op1 { test }`); collections.saveCurrentOperationAs({ name: 'operation-1', collectionName: 'collection-1', }); - laboratory.openNewTab(); - laboratory.updateEditorValue(`query op2 { test }`); + cyLaboratory.openNewTab(); + cyLaboratory.updateEditorValue(`query op2 { test }`); collections.saveCurrentOperationAs({ name: 'operation-2', collectionName: 'collection-2', @@ -243,7 +243,7 @@ describe('Laboratory > Collections', () => { return cy.visit(copiedUrl); }); - laboratory.assertActiveTab('operation-1'); - laboratory.getEditorValue().should('contain', 'op1'); + cyLaboratory.assertActiveTab('operation-1'); + cyLaboratory.getEditorValue().should('contain', 'op1'); }); }); diff --git a/cypress/e2e/laboratory-preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts similarity index 80% rename from cypress/e2e/laboratory-preflight.cy.ts rename to cypress/e2e/laboratory/preflight.cy.ts index 645dd3931f..65b69045b4 100644 --- a/cypress/e2e/laboratory-preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -1,21 +1,10 @@ -import { dedent } from '../support/testkit'; - -const selectors = { - buttonGraphiQLPreflight: '[aria-label*="Preflight Script"]', - buttonModalCy: 'preflight-modal-button', - buttonToggleCy: 'toggle-preflight', - buttonHeaders: '[data-name="headers"]', - headersEditor: { - textArea: '.graphiql-editor-tool .graphiql-editor:last-child textarea', - }, - graphiql: { - buttonExecute: '.graphiql-execute-button', - }, - - modal: { - buttonSubmitCy: 'preflight-modal-submit', - }, -}; +import { dedent } from '../../support/dedent'; +import { setMonacoEditorContents } from '../../support/monaco'; +import { cyLaboratory } from './_cy'; + +const s = cyLaboratory.preflight.selectors; + +const cyp = cyLaboratory.preflight; const data: { slug: string } = { slug: '', @@ -27,40 +16,17 @@ beforeEach(() => { cy.setCookie('sRefreshToken', refreshToken); data.slug = slug; cy.visit(`/${slug}/laboratory`); - cy.get(selectors.buttonGraphiQLPreflight).click(); + cy.get(s.buttonGraphiQLPreflight).click(); }); }); }); -/** Helper function for setting the text within a monaco editor as typing manually results in flaky tests */ -function setMonacoEditorContents(editorCyName: string, text: string) { - // wait for textarea appearing which indicates monaco is loaded - cy.dataCy(editorCyName).find('textarea'); - cy.window().then(win => { - // First, check if monaco is available on the main window - const editor = (win as any).monaco.editor - .getEditors() - .find(e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorCyName); - - // If Monaco instance is found - if (editor) { - editor.setValue(text); - } else { - throw new Error('Monaco editor not found on the window or frames[0]'); - } - }); -} - -function setEditorScript(script: string) { - setMonacoEditorContents('preflight-editor', script); -} - -describe('Laboratory > Preflight Script', () => { +describe('Preflight Tab', () => { // https://github.com/graphql-hive/console/pull/6450 it('regression: loads even if local storage is set to {}', () => { window.localStorage.setItem('hive:laboratory:environment', '{}'); cy.visit(`/${data.slug}/laboratory`); - cy.get(selectors.buttonGraphiQLPreflight).click(); + cy.get(s.buttonGraphiQLPreflight).click(); }); it('mini script editor is read only', () => { cy.dataCy('toggle-preflight').click(); @@ -76,7 +42,7 @@ describe('Laboratory > Preflight Script', () => { }); }); -describe('Preflight Script Modal', () => { +describe('Preflight Modal', () => { const script = 'console.log("Hello_world")'; const env = '{"foo":123}'; @@ -85,8 +51,22 @@ describe('Preflight Script Modal', () => { setMonacoEditorContents('env-editor', env); }); + it('code is validated with TypeScript', () => { + const tsErrorMessage = "Type 'string' is not assignable to type 'number'."; + const script = 'let a = 1; a = ""'; + cyp.setEditorContent(script); + cy.wait(1000); // :( + cy.dataCy(s.modal.editorCy) + .find('textarea') + .focus() + // Followed instructions but does not work https://github.com/dmtrKovalenko/cypress-real-events?tab=readme-ov-file#installation + // @ts-expect-error + .realPress(['Alt', 'F8']); + cy.contains(tsErrorMessage); + }); + it('save script and environment variables when submitting', () => { - setEditorScript(script); + cyp.setEditorContent(script); cy.dataCy('preflight-modal-submit').click(); cy.dataCy('env-editor-mini').should('have.text', env); cy.dataCy('toggle-preflight').click(); @@ -98,11 +78,11 @@ describe('Preflight Script Modal', () => { }); it('logs show console/error information', () => { - setEditorScript(script); + cyp.setEditorContent(script); cy.dataCy('run-preflight').click(); cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); - setEditorScript( + cyp.setEditorContent( `console.info(1) console.warn(true) console.error('Fatal') @@ -120,12 +100,12 @@ throw new TypeError('Test')`, }); it('prompt and pass the awaited response', () => { - setEditorScript(script); + cyp.setEditorContent(script); cy.dataCy('run-preflight').click(); cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); - setEditorScript( + cyp.setEditorContent( dedent` const username = await lab.prompt('Enter your username'); console.info(username); @@ -148,12 +128,12 @@ throw new TypeError('Test')`, }); it('prompt and cancel', () => { - setEditorScript(script); + cyp.setEditorContent(script); cy.dataCy('run-preflight').click(); cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); - setEditorScript( + cyp.setEditorContent( dedent` const username = await lab.prompt('Enter your username'); console.info(username); @@ -176,7 +156,7 @@ throw new TypeError('Test')`, }); it('script execution updates environment variables', () => { - setEditorScript(`lab.environment.set('my-test', "TROLOLOL")`); + cyp.setEditorContent(`lab.environment.set('my-test', "TROLOLOL")`); cy.dataCy('run-preflight').click(); cy.dataCy('env-editor').should( @@ -187,7 +167,7 @@ throw new TypeError('Test')`, }); it('`crypto-js` can be used for generating hashes', () => { - setEditorScript('console.log(lab.CryptoJS.SHA256("🐝"))'); + cyp.setEditorContent('console.log(lab.CryptoJS.SHA256("🐝"))'); cy.dataCy('run-preflight').click(); cy.dataCy('console-output').should('contain', 'info: Using crypto-js version:'); cy.dataCy('console-output').should( @@ -197,13 +177,13 @@ throw new TypeError('Test')`, }); it('scripts can not use `eval`', () => { - setEditorScript('eval()'); + cyp.setEditorContent('eval()'); cy.dataCy('preflight-modal-submit').click(); cy.get('body').contains('Usage of dangerous statement like eval() or Function("").'); }); it('invalid code is rejected and can not be saved', () => { - setEditorScript('🐝'); + cyp.setEditorContent('🐝'); cy.dataCy('preflight-modal-submit').click(); cy.get('body').contains("[1:1]: Illegal character '}"); }); @@ -215,13 +195,13 @@ describe('Execution', () => { const preflightHeaders = { foo: 'bar', }; - cy.dataCy(selectors.buttonToggleCy).click(); - cy.dataCy(selectors.buttonModalCy).click(); - setEditorScript(`lab.request.headers.append('foo', '${preflightHeaders.foo}')`); - cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.dataCy(s.buttonToggleCy).click(); + cy.dataCy(s.buttonModalCy).click(); + cyp.setEditorContent(`lab.request.headers.append('foo', '${preflightHeaders.foo}')`); + cy.dataCy(s.modal.buttonSubmitCy).click(); // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); - cy.get(selectors.graphiql.buttonExecute).click(); + cy.get(s.graphiql.buttonExecute).click(); cy.wait('@request'); }); @@ -233,19 +213,19 @@ describe('Execution', () => { accept: 'application/json, multipart/mixed', }; cy.intercept({ headers: baseHeaders }).as('integrityCheck'); - cy.get(selectors.graphiql.buttonExecute).click(); + cy.get(s.graphiql.buttonExecute).click(); cy.wait('@integrityCheck'); // Setup Preflight Script const preflightHeaders = { accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', }; - cy.dataCy(selectors.buttonToggleCy).click(); - cy.dataCy(selectors.buttonModalCy).click(); - setEditorScript(`lab.request.headers.append('accept', '${preflightHeaders.accept}')`); - cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.dataCy(s.buttonToggleCy).click(); + cy.dataCy(s.buttonModalCy).click(); + cyp.setEditorContent(`lab.request.headers.append('accept', '${preflightHeaders.accept}')`); + cy.dataCy(s.modal.buttonSubmitCy).click(); // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); - cy.get(selectors.graphiql.buttonExecute).click(); + cy.get(s.graphiql.buttonExecute).click(); cy.wait('@request'); }); @@ -255,8 +235,8 @@ describe('Execution', () => { const staticHeaders = { foo_static: barEnVarInterpolation, }; - cy.get(selectors.buttonHeaders).click(); - cy.get(selectors.headersEditor.textArea).type(JSON.stringify(staticHeaders), { + cy.get(s.buttonHeaders).click(); + cy.get(s.headersEditor.textArea).type(JSON.stringify(staticHeaders), { force: true, parseSpecialCharSequences: false, }); @@ -267,13 +247,13 @@ describe('Execution', () => { const preflightHeaders = { foo_preflight: barEnVarInterpolation, }; - cy.dataCy(selectors.buttonToggleCy).click(); - cy.dataCy(selectors.buttonModalCy).click(); - setEditorScript(` + cy.dataCy(s.buttonToggleCy).click(); + cy.dataCy(s.buttonModalCy).click(); + cyp.setEditorContent(` lab.environment.set('bar', '${environmentVariables.bar}') lab.request.headers.append('foo_preflight', '${preflightHeaders.foo_preflight}') `); - cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.dataCy(s.modal.buttonSubmitCy).click(); // Run GraphiQL cy.intercept({ headers: { @@ -281,7 +261,7 @@ describe('Execution', () => { foo_static: environmentVariables.bar, }, }).as('request'); - cy.get(selectors.graphiql.buttonExecute).click(); + cy.get(s.graphiql.buttonExecute).click(); cy.wait('@request'); }); @@ -323,7 +303,7 @@ describe('Execution', () => { }, ); cy.dataCy('preflight-modal-button').click(); - setMonacoEditorContents('preflight-editor', `lab.environment.set('foo', '92')`); + cyp.setEditorContent(`lab.environment.set('foo', '92')`); cy.dataCy('preflight-modal-submit').click(); cy.intercept({ @@ -350,8 +330,7 @@ describe('Execution', () => { ); cy.dataCy('preflight-modal-button').click(); - setMonacoEditorContents( - 'preflight-editor', + cyp.setEditorContent( dedent` const username = await lab.prompt('Enter your username'); lab.environment.set('username', username); diff --git a/cypress/e2e/laboratory-tabs.cy.ts b/cypress/e2e/laboratory/tabs.cy.ts similarity index 56% rename from cypress/e2e/laboratory-tabs.cy.ts rename to cypress/e2e/laboratory/tabs.cy.ts index 1f68000cc5..ea063e0c63 100644 --- a/cypress/e2e/laboratory-tabs.cy.ts +++ b/cypress/e2e/laboratory/tabs.cy.ts @@ -1,4 +1,4 @@ -import { laboratory } from '../support/testkit'; +import { cyLaboratory } from './_cy'; beforeEach(() => { cy.clearAllLocalStorage().then(() => { @@ -16,21 +16,21 @@ describe('Laboratory > Tabs', () => { const op2 = 'query { tab2 }'; // make sure there's only one tab - laboratory.closeTabsUntilOneLeft(); - laboratory.updateEditorValue(op1); - laboratory.getEditorValue().should('eq', op1); + cyLaboratory.closeTabsUntilOneLeft(); + cyLaboratory.updateEditorValue(op1); + cyLaboratory.getEditorValue().should('eq', op1); // open a new tab and update its value - laboratory.openNewTab(); - laboratory.updateEditorValue(op2); - laboratory.getEditorValue().should('eq', op2); + cyLaboratory.openNewTab(); + cyLaboratory.updateEditorValue(op2); + cyLaboratory.getEditorValue().should('eq', op2); // close the second tab - laboratory.closeActiveTab(); - laboratory.getEditorValue().should('eq', op1); + cyLaboratory.closeActiveTab(); + cyLaboratory.getEditorValue().should('eq', op1); // close the first tab - laboratory.closeActiveTab(); + cyLaboratory.closeActiveTab(); // it should reset the editor to its default state - laboratory.getEditorValue().should('not.eq', op1); + cyLaboratory.getEditorValue().should('not.eq', op1); }); }); diff --git a/cypress/support/dedent.ts b/cypress/support/dedent.ts new file mode 100644 index 0000000000..52ae660c88 --- /dev/null +++ b/cypress/support/dedent.ts @@ -0,0 +1,52 @@ +export function dedent(strings: TemplateStringsArray, ...values: unknown[]): string { + // Took from https://github.com/dmnd/dedent + // Couldn't use the package because I had some issues with moduleResolution. + const raw = strings.raw; + + // first, perform interpolation + let result = ''; + for (let i = 0; i < raw.length; i++) { + let next = raw[i]; + + // handle escaped newlines, backticks, and interpolation characters + next = next + .replace(/\\\n[ \t]*/g, '') + .replace(/\\`/g, '`') + .replace(/\\\$/g, '$') + .replace(/\\\{/g, '{'); + + result += next; + + if (i < values.length) { + result += values[i]; + } + } + + // now strip indentation + const lines = result.split('\n'); + let mindent: null | number = null; + for (const l of lines) { + const m = l.match(/^(\s+)\S+/); + if (m) { + const indent = m[1].length; + if (!mindent) { + // this is the first indented line + mindent = indent; + } else { + mindent = Math.min(mindent, indent); + } + } + } + + if (mindent !== null) { + const m = mindent; // appease TypeScript + result = lines.map(l => (l[0] === ' ' || l[0] === '\t' ? l.slice(m) : l)).join('\n'); + } + + // dedent eats leading and trailing whitespace too + result = result.trim(); + // handle escaped newlines at the end to ensure they don't get stripped too + result = result.replace(/\\n/g, '\n'); + + return result; +} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 7149352f73..bf411a0a42 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,4 +1,5 @@ import './commands'; +import 'cypress-real-events'; Cypress.on('uncaught:exception', (_err, _runnable) => { return false; diff --git a/cypress/support/monaco.ts b/cypress/support/monaco.ts new file mode 100644 index 0000000000..878ea9e13b --- /dev/null +++ b/cypress/support/monaco.ts @@ -0,0 +1,18 @@ +/** Helper function for setting the text within a monaco editor as typing manually results in flaky tests */ +export function setMonacoEditorContents(editorCyName: string, text: string) { + // wait for textarea appearing which indicates monaco is loaded + cy.dataCy(editorCyName).find('textarea'); + cy.window().then(win => { + // First, check if monaco is available on the main window + const editor = (win as any).monaco.editor + .getEditors() + .find(e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorCyName); + + // If Monaco instance is found + if (editor) { + editor.setValue(text); + } else { + throw new Error('Monaco editor not found on the window or frames[0]'); + } + }); +} diff --git a/cypress/support/testkit.ts b/cypress/support/testkit.ts index 44f20faae2..10e09ca0d6 100644 --- a/cypress/support/testkit.ts +++ b/cypress/support/testkit.ts @@ -37,102 +37,3 @@ export function createProject(projectSlug: string) { cy.get('form[data-cy="create-project-form"] [data-cy="slug"]').type(projectSlug); cy.get('form[data-cy="create-project-form"] [data-cy="submit"]').click(); } - -export const laboratory = { - /** - * Updates the value of the graphiql editor - */ - updateEditorValue(value: string) { - cy.get('.graphiql-query-editor .cm-s-graphiql').then($editor => { - const editor = ($editor[0] as any).CodeMirror; // Access the CodeMirror instance - editor.setValue(value); - }); - }, - /** - * Returns the value of the graphiql editor as Chainable - */ - getEditorValue() { - return cy.get('.graphiql-query-editor .cm-s-graphiql').then($editor => { - const editor = ($editor[0] as any).CodeMirror; // Access the CodeMirror instance - return editor.getValue(); - }); - }, - openNewTab() { - cy.get('button[aria-label="New tab"]').click(); - // tab's title should be "untitled" as it's a default name - cy.contains('button[aria-controls="graphiql-session"]', 'untitled').should('exist'); - }, - /** - * Asserts that the tab with the given name is active - */ - assertActiveTab(name: string) { - cy.contains('li.graphiql-tab-active > button[aria-controls="graphiql-session"]', name).should( - 'exist', - ); - }, - closeActiveTab() { - cy.get('li.graphiql-tab-active > button.graphiql-tab-close').click(); - }, - closeTabsUntilOneLeft() { - cy.get('li.graphiql-tab').then($tabs => { - if ($tabs.length > 1) { - laboratory.closeActiveTab(); - // Recurse until there's only one tab left - return laboratory.closeTabsUntilOneLeft(); - } - }); - }, -}; - -export function dedent(strings: TemplateStringsArray, ...values: unknown[]): string { - // Took from https://github.com/dmnd/dedent - // Couldn't use the package because I had some issues with moduleResolution. - const raw = strings.raw; - - // first, perform interpolation - let result = ''; - for (let i = 0; i < raw.length; i++) { - let next = raw[i]; - - // handle escaped newlines, backticks, and interpolation characters - next = next - .replace(/\\\n[ \t]*/g, '') - .replace(/\\`/g, '`') - .replace(/\\\$/g, '$') - .replace(/\\\{/g, '{'); - - result += next; - - if (i < values.length) { - result += values[i]; - } - } - - // now strip indentation - const lines = result.split('\n'); - let mindent: null | number = null; - for (const l of lines) { - const m = l.match(/^(\s+)\S+/); - if (m) { - const indent = m[1].length; - if (!mindent) { - // this is the first indented line - mindent = indent; - } else { - mindent = Math.min(mindent, indent); - } - } - } - - if (mindent !== null) { - const m = mindent; // appease TypeScript - result = lines.map(l => (l[0] === ' ' || l[0] === '\t' ? l.slice(m) : l)).join('\n'); - } - - // dedent eats leading and trailing whitespace too - result = result.trim(); - // handle escaped newlines at the end to ensure they don't get stripped too - result = result.replace(/\\n/g, '\n'); - - return result; -} diff --git a/package.json b/package.json index aec78fcd25..76ccd5d735 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "test": "vitest", "test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run --browser chrome", "test:e2e:open": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress open", + "test:e2e:local": "CYPRESS_BASE_URL=http://localhost:3000 RUN_AGAINST_LOCAL_SERVICES=1 cypress open --browser chrome", "test:integration": "cd integration-tests && pnpm test:integration", "typecheck": "pnpm run -r --filter '!hive' typecheck", "upload-sourcemaps": "./scripts/upload-sourcemaps.sh", @@ -76,6 +77,7 @@ "@types/node": "22.10.5", "bob-the-bundler": "7.0.1", "cypress": "13.17.0", + "cypress-real-events": "^1.14.0", "dotenv": "16.4.7", "eslint": "8.57.1", "eslint-plugin-cypress": "4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5590902894..52dda29620 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,9 @@ importers: cypress: specifier: 13.17.0 version: 13.17.0 + cypress-real-events: + specifier: ^1.14.0 + version: 1.14.0(cypress@13.17.0) dotenv: specifier: 16.4.7 version: 16.4.7 @@ -3908,6 +3911,7 @@ packages: '@fastify/vite@6.0.7': resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} + bundledDependencies: [] '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} @@ -9380,6 +9384,11 @@ packages: csv-stringify@6.5.2: resolution: {integrity: sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==} + cypress-real-events@1.14.0: + resolution: {integrity: sha512-XmI8y3OZLh6cjRroPalzzS++iv+pGCaD9G9kfIbtspgv7GVsDt30dkZvSXfgZb4rAN+3pOkMVB7e0j4oXydW7Q==} + peerDependencies: + cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x || ^14.x + cypress@13.17.0: resolution: {integrity: sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} @@ -26065,6 +26074,10 @@ snapshots: csv-stringify@6.5.2: {} + cypress-real-events@1.14.0(cypress@13.17.0): + dependencies: + cypress: 13.17.0 + cypress@13.17.0: dependencies: '@cypress/request': 3.0.6 diff --git a/tsconfig.json b/tsconfig.json index 5b8a1f8313..4090e466d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "module": "esnext", "target": "esnext", "lib": ["esnext", "dom"], - "types": ["vitest/globals"], + "types": ["vitest/globals", "cypress-real-events"], "baseUrl": ".", "outDir": "dist", "rootDir": ".", From 2d772e4913b4604e9aea954ae16980d9f7853b4f Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 12:41:16 -0500 Subject: [PATCH 03/21] doc rationale --- cypress/support/e2e.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index bf411a0a42..bca30a348c 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,4 +1,7 @@ import './commands'; +// Cypress does not support real events, arbitrary keyboard input. +// @see https://github.com/cypress-io/cypress/discussions/19790 +// We use this for pressing Alt+F8 in Preflight editor. import 'cypress-real-events'; Cypress.on('uncaught:exception', (_err, _runnable) => { From ce4c80ffc72b785704e15574fe42b8d25f966f25 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 13:01:31 -0500 Subject: [PATCH 04/21] lint --- cypress/e2e/laboratory/preflight.cy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 65b69045b4..14edcb61c2 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -59,8 +59,7 @@ describe('Preflight Modal', () => { cy.dataCy(s.modal.editorCy) .find('textarea') .focus() - // Followed instructions but does not work https://github.com/dmtrKovalenko/cypress-real-events?tab=readme-ov-file#installation - // @ts-expect-error + // @ts-expect-error - Followed instructions but does not work https://github.com/dmtrKovalenko/cypress-real-events?tab=readme-ov-file#installation .realPress(['Alt', 'F8']); cy.contains(tsErrorMessage); }); From 7dfbe35c7c5c79fd42e10347aef3044839db3e64 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 13:02:29 -0500 Subject: [PATCH 05/21] pin --- package.json | 2 +- pnpm-lock.yaml | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 0419d1007b..84fbabd782 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@types/node": "22.10.5", "bob-the-bundler": "7.0.1", "cypress": "13.17.0", - "cypress-real-events": "^1.14.0", + "cypress-real-events": "1.14.0", "dotenv": "16.4.7", "eslint": "8.57.1", "eslint-plugin-cypress": "4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 916c548c06..abda47865b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,7 +138,7 @@ importers: specifier: 13.17.0 version: 13.17.0 cypress-real-events: - specifier: ^1.14.0 + specifier: 1.14.0 version: 1.14.0(cypress@13.17.0) dotenv: specifier: 16.4.7 @@ -16537,8 +16537,8 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) - '@aws-sdk/client-sts': 3.596.0 + '@aws-sdk/client-sso-oidc': 3.596.0 + '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16645,11 +16645,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)': + '@aws-sdk/client-sso-oidc@3.596.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.596.0 + '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16688,7 +16688,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.723.0(@aws-sdk/client-sts@3.723.0)': @@ -16822,11 +16821,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.596.0': + '@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) + '@aws-sdk/client-sso-oidc': 3.596.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16865,6 +16864,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.723.0': @@ -16978,7 +16978,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0 + '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) '@aws-sdk/credential-provider-env': 3.587.0 '@aws-sdk/credential-provider-http': 3.596.0 '@aws-sdk/credential-provider-process': 3.587.0 @@ -17097,7 +17097,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0 + '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/types': 3.7.2 @@ -17272,7 +17272,7 @@ snapshots: '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) + '@aws-sdk/client-sso-oidc': 3.596.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 From 3bcfb5dee8c198ebc9402a724021191517866b94 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 13:12:32 -0500 Subject: [PATCH 06/21] fix --- cypress/e2e/laboratory/preflight.cy.ts | 6 +----- cypress/tsconfig.json | 2 +- tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 14edcb61c2..08eadb2b0e 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -56,11 +56,7 @@ describe('Preflight Modal', () => { const script = 'let a = 1; a = ""'; cyp.setEditorContent(script); cy.wait(1000); // :( - cy.dataCy(s.modal.editorCy) - .find('textarea') - .focus() - // @ts-expect-error - Followed instructions but does not work https://github.com/dmtrKovalenko/cypress-real-events?tab=readme-ov-file#installation - .realPress(['Alt', 'F8']); + cy.dataCy(s.modal.editorCy).find('textarea').focus().realPress(['Alt', 'F8']); cy.contains(tsErrorMessage); }); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index f545e65eae..fe6507f941 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es2021", "lib": ["es2021", "dom"], - "types": ["node", "cypress"] + "types": ["node", "cypress", "cypress-real-events"] }, "include": ["**/*.ts", "../integration-tests/testkit/**/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 4090e466d2..5b8a1f8313 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "module": "esnext", "target": "esnext", "lib": ["esnext", "dom"], - "types": ["vitest/globals", "cypress-real-events"], + "types": ["vitest/globals"], "baseUrl": ".", "outDir": "dist", "rootDir": ".", From 78d4a877c38f9200217e02518617048b6bd46496 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 13:16:35 -0500 Subject: [PATCH 07/21] fix type cast any --- cypress/support/monaco.ts | 6 ++++-- cypress/tsconfig.json | 3 ++- package.json | 1 + pnpm-lock.yaml | 24 +++++++++++++----------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/cypress/support/monaco.ts b/cypress/support/monaco.ts index 878ea9e13b..7636c401e7 100644 --- a/cypress/support/monaco.ts +++ b/cypress/support/monaco.ts @@ -1,10 +1,12 @@ +import type * as Monaco from 'monaco-editor'; + /** Helper function for setting the text within a monaco editor as typing manually results in flaky tests */ export function setMonacoEditorContents(editorCyName: string, text: string) { // wait for textarea appearing which indicates monaco is loaded cy.dataCy(editorCyName).find('textarea'); - cy.window().then(win => { + cy.window().then((win: Window & typeof globalThis & { monaco: typeof Monaco }) => { // First, check if monaco is available on the main window - const editor = (win as any).monaco.editor + const editor = win.monaco.editor .getEditors() .find(e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorCyName); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index fe6507f941..f1dc7b5842 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "target": "es2021", "lib": ["es2021", "dom"], - "types": ["node", "cypress", "cypress-real-events"] + "moduleResolution": "node", + "types": ["node", "cypress", "cypress-real-events", "monaco-editor"] }, "include": ["**/*.ts", "../integration-tests/testkit/**/*.ts"] } diff --git a/package.json b/package.json index 84fbabd782..93ce4a1a45 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "graphql": "16.9.0", "gray-matter": "4.0.3", "jest-snapshot-serializer-raw": "2.0.0", + "monaco-editor": "0.52.2", "pg": "8.13.1", "prettier": "3.4.2", "prettier-plugin-sql": "0.18.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abda47865b..93ab119ddf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ importers: jest-snapshot-serializer-raw: specifier: 2.0.0 version: 2.0.0 + monaco-editor: + specifier: 0.52.2 + version: 0.52.2 pg: specifier: 8.13.1 version: 8.13.1 @@ -3911,7 +3914,6 @@ packages: '@fastify/vite@6.0.7': resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} - bundledDependencies: [] '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} @@ -16537,8 +16539,8 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0 - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16645,11 +16647,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.596.0': + '@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16688,6 +16690,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: + - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.723.0(@aws-sdk/client-sts@3.723.0)': @@ -16821,11 +16824,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)': + '@aws-sdk/client-sts@3.596.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0 + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16864,7 +16867,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.723.0': @@ -16978,7 +16980,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/credential-provider-env': 3.587.0 '@aws-sdk/credential-provider-http': 3.596.0 '@aws-sdk/credential-provider-process': 3.587.0 @@ -17097,7 +17099,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/types': 3.7.2 @@ -17272,7 +17274,7 @@ snapshots: '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.596.0 + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 From 2b725b21c70c10fc5d202bea9e81cbdc582e747b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 13:25:11 -0500 Subject: [PATCH 08/21] feedback --- cypress/e2e/laboratory/_cy.ts | 12 ++++++++++-- cypress/e2e/laboratory/preflight.cy.ts | 13 +++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/laboratory/_cy.ts b/cypress/e2e/laboratory/_cy.ts index f19d690ab3..61ce189fd3 100644 --- a/cypress/e2e/laboratory/_cy.ts +++ b/cypress/e2e/laboratory/_cy.ts @@ -75,13 +75,21 @@ export namespace cyLaboratory { modal: { buttonSubmitCy: 'preflight-modal-submit', editorCy: 'preflight-editor', + variablesEditorCy: 'env-editor', }, }; /** * Sets the content of the preflight editor */ - export function setEditorContent(value: string) { + export const setEditorContent = (value: string) => { setMonacoEditorContents(selectors.modal.editorCy, value); - } + }; + + /** + * Sets the content of the variables editor + */ + export const setEnvironmentEditorContent = (value: string) => { + setMonacoEditorContents(selectors.modal.variablesEditorCy, value); + }; } } diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 08eadb2b0e..8a793a922d 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -1,5 +1,4 @@ import { dedent } from '../../support/dedent'; -import { setMonacoEditorContents } from '../../support/monaco'; import { cyLaboratory } from './_cy'; const s = cyLaboratory.preflight.selectors; @@ -48,7 +47,7 @@ describe('Preflight Modal', () => { beforeEach(() => { cy.dataCy('preflight-modal-button').click(); - setMonacoEditorContents('env-editor', env); + cyp.setEnvironmentEditorContent(env); }); it('code is validated with TypeScript', () => { @@ -357,8 +356,8 @@ describe('Execution', () => { }, ); cy.dataCy('preflight-modal-button').click(); - setMonacoEditorContents('preflight-editor', `lab.environment.set('foo', 92)`); - setMonacoEditorContents('env-editor', `{"foo":10}`); + cyp.setEditorContent(`lab.environment.set('foo', 92)`); + cyp.setEnvironmentEditorContent(`{"foo":10}`); cy.dataCy('preflight-modal-submit').click(); @@ -376,8 +375,7 @@ describe('Execution', () => { cy.dataCy('toggle-preflight').click(); cy.dataCy('preflight-modal-button').click(); - setMonacoEditorContents( - 'preflight-editor', + cyp.setEditorContent( dedent` console.info(1) console.warn(true) @@ -421,8 +419,7 @@ describe('Execution', () => { cy.dataCy('toggle-preflight').click(); cy.dataCy('preflight-modal-button').click(); - setMonacoEditorContents( - 'preflight-editor', + cyp.setEditorContent( dedent` console.info(1) console.warn(true) From 09077d0d6b7b5828a46b69f9f5ff804d0acf0140 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 14:23:07 -0500 Subject: [PATCH 09/21] lock --- pnpm-lock.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93ab119ddf..a7b70622b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3914,6 +3914,7 @@ packages: '@fastify/vite@6.0.7': resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} + bundledDependencies: [] '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} From ebe9be3990d1597cbe8fe62869d9a000f1ac751f Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 6 Feb 2025 14:33:01 -0500 Subject: [PATCH 10/21] mention hack --- cypress/e2e/laboratory/preflight.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 8a793a922d..3e7d1b8c0e 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -54,7 +54,8 @@ describe('Preflight Modal', () => { const tsErrorMessage = "Type 'string' is not assignable to type 'number'."; const script = 'let a = 1; a = ""'; cyp.setEditorContent(script); - cy.wait(1000); // :( + // Hack: Seemingly only way to reliably interact with the monaco text area from Cypress. + cy.wait(1000); cy.dataCy(s.modal.editorCy).find('textarea').focus().realPress(['Alt', 'F8']); cy.contains(tsErrorMessage); }); From 7692fc4a142b762061207fd850fc171686f79310 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Fri, 7 Feb 2025 11:12:15 -0500 Subject: [PATCH 11/21] wip --- .../app/src/lib/MonacoEditorReact/index.ts | 3 + .../lib/preflight/components/EditorTitle.tsx | 10 + .../components/EnvironmentEditor.tsx | 25 ++ .../src/lib/preflight/components/LogLine.tsx | 29 ++ .../preflight/components/PreflightModal.tsx | 305 +++++++++++++++ .../lib/preflight/components/ScriptEditor.tsx | 30 ++ .../components/_defaultEditorProps.ts | 17 + .../app/src/lib/preflight/graphiql-plugin.tsx | 350 +----------------- packages/web/app/src/lib/preflight/index.ts | 3 + .../web/app/src/lib/preflight/shared-types.ts | 8 + .../web/app/src/pages/target-laboratory.tsx | 2 +- 11 files changed, 445 insertions(+), 337 deletions(-) create mode 100644 packages/web/app/src/lib/MonacoEditorReact/index.ts create mode 100644 packages/web/app/src/lib/preflight/components/EditorTitle.tsx create mode 100644 packages/web/app/src/lib/preflight/components/EnvironmentEditor.tsx create mode 100644 packages/web/app/src/lib/preflight/components/LogLine.tsx create mode 100644 packages/web/app/src/lib/preflight/components/PreflightModal.tsx create mode 100644 packages/web/app/src/lib/preflight/components/ScriptEditor.tsx create mode 100644 packages/web/app/src/lib/preflight/components/_defaultEditorProps.ts create mode 100644 packages/web/app/src/lib/preflight/index.ts diff --git a/packages/web/app/src/lib/MonacoEditorReact/index.ts b/packages/web/app/src/lib/MonacoEditorReact/index.ts new file mode 100644 index 0000000000..81f3ecce7c --- /dev/null +++ b/packages/web/app/src/lib/MonacoEditorReact/index.ts @@ -0,0 +1,3 @@ +import * as MonacoEditorReact from '@monaco-editor/react'; + +export { MonacoEditorReact }; diff --git a/packages/web/app/src/lib/preflight/components/EditorTitle.tsx b/packages/web/app/src/lib/preflight/components/EditorTitle.tsx new file mode 100644 index 0000000000..3525462904 --- /dev/null +++ b/packages/web/app/src/lib/preflight/components/EditorTitle.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; + +export function EditorTitle(props: { children: ReactNode; className?: string }) { + return ( +
+ {props.children} +
+ ); +} diff --git a/packages/web/app/src/lib/preflight/components/EnvironmentEditor.tsx b/packages/web/app/src/lib/preflight/components/EnvironmentEditor.tsx new file mode 100644 index 0000000000..05f3d24626 --- /dev/null +++ b/packages/web/app/src/lib/preflight/components/EnvironmentEditor.tsx @@ -0,0 +1,25 @@ +import { Editor, EditorProps } from '@monaco-editor/react'; +import { defaultEditorProps } from './_defaultEditorProps'; + +export const defaultProps: Readonly = { + ...defaultEditorProps, + defaultLanguage: 'json', + options: { + ...defaultEditorProps.options, + lineNumbers: 'off', + tabSize: 2, + }, +}; + +export const EnvironmentEditor: React.FC = props => { + return ( + + ); +}; diff --git a/packages/web/app/src/lib/preflight/components/LogLine.tsx b/packages/web/app/src/lib/preflight/components/LogLine.tsx new file mode 100644 index 0000000000..54c46252a9 --- /dev/null +++ b/packages/web/app/src/lib/preflight/components/LogLine.tsx @@ -0,0 +1,29 @@ +import { captureException } from '@sentry/react'; +import { LogRecord } from '../shared-types'; + +export function LogLine({ log }: { log: LogRecord }) { + if ('type' in log && log.type === 'separator') { + return
; + } + + if ('level' in log && log.level in LOG_COLORS) { + return ( +
+ {log.level}: {log.message} + {log.line && log.column ? ` (${log.line}:${log.column})` : ''} +
+ ); + } + + captureException(new Error('Unexpected log type in Preflight Script output'), { + extra: { log }, + }); + return null; +} + +const LOG_COLORS = { + error: 'text-red-400', + info: 'text-emerald-400', + warn: 'text-yellow-400', + log: 'text-gray-400', +}; diff --git a/packages/web/app/src/lib/preflight/components/PreflightModal.tsx b/packages/web/app/src/lib/preflight/components/PreflightModal.tsx new file mode 100644 index 0000000000..887d65d43a --- /dev/null +++ b/packages/web/app/src/lib/preflight/components/PreflightModal.tsx @@ -0,0 +1,305 @@ +import { useCallback, useEffect, useRef } from 'react'; +import type { editor } from 'monaco-editor'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { MonacoEditorReact } from '@/lib/MonacoEditorReact'; +import { Cross2Icon, InfoCircledIcon, TriangleRightIcon } from '@radix-ui/react-icons'; +import labApiDefinitionRaw from '../lab-api-declaration?raw'; +import { LogRecord, PreflightWorkerState } from '../shared-types'; +import { EditorTitle } from './EditorTitle'; +import { EnvironmentEditor } from './EnvironmentEditor'; +import { LogLine } from './LogLine'; +import { ScriptEditor } from './ScriptEditor'; + +export function PreflightModal({ + isOpen, + toggle, + content, + state, + execute, + abortExecution, + logs, + clearLogs, + onContentChange, + envValue, + onEnvValueChange, +}: { + isOpen: boolean; + toggle: () => void; + content?: string; + state: PreflightWorkerState; + execute: (script: string) => void; + abortExecution: () => void; + logs: Array; + clearLogs: () => void; + onContentChange: (value: string) => void; + envValue: string; + onEnvValueChange: (value: string) => void; +}) { + const scriptEditorRef = useRef(null); + const envEditorRef = useRef(null); + const consoleRef = useRef(null); + + const handleScriptEditorDidMount: MonacoEditorReact.OnMount = useCallback(editor => { + scriptEditorRef.current = editor; + }, []); + + const handleEnvEditorDidMount: MonacoEditorReact.OnMount = useCallback(editor => { + envEditorRef.current = editor; + }, []); + + const handleMonacoEditorBeforeMount = useCallback((monaco: MonacoEditorReact.Monaco) => { + // Configure JavaScript defaults for TypeScript validation + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false, + noSuggestionDiagnostics: false, + diagnosticCodesToIgnore: [], // Can specify codes to ignore + }); + // monaco.languages.typescript. + + // Enable modern JavaScript features and strict checks + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + allowNonTsExtensions: true, + allowJs: true, + checkJs: true, + // noEmit: true, + target: monaco.languages.typescript.ScriptTarget.ES2020, + // moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + // module: monaco.languages.typescript.ModuleKind.ESNext, + lib: [], + // types: [], + strict: true, + noUnusedLocals: true, + noUnusedParameters: true, + noImplicitReturns: true, + noFallthroughCasesInSwitch: true, + }); + + // Add custom typings for globalThis + monaco.languages.typescript.javascriptDefaults.addExtraLib( + ` + /// + + ${labApiDefinitionRaw} + declare const lab: LabAPI; + + + // ------------------------------------------------------------------------------------------------ + // The following declarations are taken from: + // https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts + // ------------------------------------------------------------------------------------------------ + + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */ + interface Console { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/assert_static) */ + assert(condition?: boolean, ...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) */ + clear(): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */ + count(label?: string): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) */ + countReset(label?: string): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */ + debug(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) */ + dir(item?: any, options?: any): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) */ + dirxml(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) */ + error(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) */ + group(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) */ + groupCollapsed(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) */ + groupEnd(): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */ + info(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) */ + log(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) */ + table(tabularData?: any, properties?: string[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) */ + time(label?: string): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) */ + timeEnd(label?: string): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) */ + timeLog(label?: string, ...data: any[]): void; + timeStamp(label?: string): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) */ + trace(...data: any[]): void; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) */ + warn(...data: any[]): void; + } + declare const console: Console; + + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ + declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; + + type TimerHandler = string | Function; + `, + 'global.d.ts', + ); + }, []); + + const handleSubmit = useCallback(() => { + onContentChange(scriptEditorRef.current?.getValue() ?? ''); + onEnvValueChange(envEditorRef.current?.getValue() ?? ''); + toggle(); + }, []); + + useEffect(() => { + const consoleEl = consoleRef.current; + consoleEl?.scroll({ top: consoleEl.scrollHeight, behavior: 'smooth' }); + }, [logs]); + + return ( + { + if (!open) { + abortExecution(); + } + toggle(); + }} + > + { + // prevent pressing escape in monaco to close the modal + if (ev.target instanceof HTMLTextAreaElement) { + ev.preventDefault(); + } + }} + > + + Edit your Preflight Script + + This script will run in each user's browser and be stored in plain text on our servers. + Don't share any secrets here. +
+ All team members can view the script and toggle it off when they need to. +
+
+
+
+
+ + Script Editor + + JavaScript + + + +
+ +
+
+
+ Console Output + +
+
+ {logs.map((log, index) => ( + + ))} +
+ + Environment Variables + + JSON + + + onEnvValueChange(value ?? '')} + onMount={handleEnvEditorDidMount} + options={{ + wordWrap: 'wordWrapColumn', + }} + wrapperProps={{ + ['data-cy']: 'env-editor', + }} + /> +
+
+ +

+ + Changes made to this Preflight Script will apply to all users on your team using this + target. +

+ + +
+
+
+ ); +} diff --git a/packages/web/app/src/lib/preflight/components/ScriptEditor.tsx b/packages/web/app/src/lib/preflight/components/ScriptEditor.tsx new file mode 100644 index 0000000000..a9f26941ff --- /dev/null +++ b/packages/web/app/src/lib/preflight/components/ScriptEditor.tsx @@ -0,0 +1,30 @@ +import { Editor, EditorProps } from '@monaco-editor/react'; +import { defaultEditorProps } from './_defaultEditorProps'; + +export const defaultProps: Readonly = { + ...defaultEditorProps, + defaultLanguage: 'javascript', + language: 'javascript', + options: { + ...defaultEditorProps.options, + quickSuggestions: true, + suggestOnTriggerCharacters: true, + acceptSuggestionOnEnter: 'on', + tabCompletion: 'on', + folding: true, + foldingStrategy: 'indentation', + }, +}; + +export const ScriptEditor: React.FC = props => { + return ( + + ); +}; diff --git a/packages/web/app/src/lib/preflight/components/_defaultEditorProps.ts b/packages/web/app/src/lib/preflight/components/_defaultEditorProps.ts new file mode 100644 index 0000000000..5f48d77eec --- /dev/null +++ b/packages/web/app/src/lib/preflight/components/_defaultEditorProps.ts @@ -0,0 +1,17 @@ +import clsx from 'clsx'; +import { EditorProps } from '@monaco-editor/react'; + +export const defaultEditorProps: Readonly = { + theme: 'vs-dark', + className: clsx('*:bg-[#10151f]'), + options: { + minimap: { enabled: false }, + padding: { + top: 10, + }, + scrollbar: { + horizontalScrollbarSize: 6, + verticalScrollbarSize: 6, + }, + }, +}; diff --git a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx index a08108cea1..0020e3f673 100644 --- a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx @@ -10,36 +10,34 @@ import { } from 'react'; import { clsx } from 'clsx'; import { PowerIcon } from 'lucide-react'; -import type { editor } from 'monaco-editor'; import { useMutation } from 'urql'; import { z } from 'zod'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Subtitle } from '@/components/ui/page'; import { usePromptManager } from '@/components/ui/prompt'; import { useToast } from '@/components/ui/use-toast'; import { FragmentType, graphql, useFragment } from '@/gql'; import { useLocalStorage, useLocalStorageJson, useToggle } from '@/lib/hooks'; import { GraphiQLPlugin } from '@graphiql/react'; -import { Editor as MonacoEditor, OnMount, type Monaco } from '@monaco-editor/react'; -import { Cross2Icon, InfoCircledIcon, Pencil1Icon, TriangleRightIcon } from '@radix-ui/react-icons'; -import { captureException } from '@sentry/react'; +import { Pencil1Icon } from '@radix-ui/react-icons'; import { useParams } from '@tanstack/react-router'; import { Kit } from '../kit'; import { cn } from '../utils'; -import labApiDefinitionRaw from './lab-api-declaration?raw'; -import { IFrameEvents, LogMessage } from './shared-types'; +import { EditorTitle } from './components/EditorTitle'; +import { EnvironmentEditor } from './components/EnvironmentEditor'; +import { PreflightModal } from './components/PreflightModal'; +import { ScriptEditor } from './components/ScriptEditor'; +import { IFrameEvents, LogRecord, PreflightWorkerState } from './shared-types'; export type PreflightResultData = Omit; +const classes = { + monacoMini: clsx('h-32 *:rounded-md *:bg-[#10151f]'), + // todo: was unused, commented out for now, remove? + // icon: clsx('absolute -left-5 top-px'), +}; + export const preflightPlugin: GraphiQLPlugin = { icon: () => ( - {props.children} - - ); -} - -const sharedMonacoProps = { - theme: 'vs-dark', - className: classes.monaco, - options: { - minimap: { enabled: false }, - padding: { - top: 10, - }, - scrollbar: { - horizontalScrollbarSize: 6, - verticalScrollbarSize: 6, - }, - }, -} satisfies ComponentPropsWithoutRef; - -const monacoProps = { - env: { - ...sharedMonacoProps, - defaultLanguage: 'json', - options: { - ...sharedMonacoProps.options, - lineNumbers: 'off', - tabSize: 2, - }, - }, - script: { - ...sharedMonacoProps, - theme: 'vs-dark', - defaultLanguage: 'javascript', - options: { - ...sharedMonacoProps.options, - quickSuggestions: true, - suggestOnTriggerCharacters: true, - acceptSuggestionOnEnter: 'on', - tabCompletion: 'on', - folding: true, - foldingStrategy: 'indentation', - }, - }, -} satisfies Record<'script' | 'env', ComponentPropsWithoutRef>; - const UpdatePreflightScriptMutation = graphql(` mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) { updatePreflightScript(input: $input) { @@ -143,13 +86,6 @@ const PreflightScript_TargetFragment = graphql(` } `); -export type LogRecord = LogMessage | { type: 'separator' }; - -export const enum PreflightWorkerState { - running, - ready, -} - export function usePreflight(args: { target: FragmentType | null; }) { @@ -564,16 +500,14 @@ function PreflightContent() { )} - Declare variables that can be used by both the script and headers. - preflight.setEnvironmentVariables(value ?? '')} - {...monacoProps.env} className={classes.monacoMini} wrapperProps={{ ['data-cy']: 'env-editor-mini', @@ -606,258 +539,3 @@ function PreflightContent() { ); } - -function PreflightModal({ - isOpen, - toggle, - content, - state, - execute, - abortExecution, - logs, - clearLogs, - onContentChange, - envValue, - onEnvValueChange, -}: { - isOpen: boolean; - toggle: () => void; - content?: string; - state: PreflightWorkerState; - execute: (script: string) => void; - abortExecution: () => void; - logs: Array; - clearLogs: () => void; - onContentChange: (value: string) => void; - envValue: string; - onEnvValueChange: (value: string) => void; -}) { - const scriptEditorRef = useRef(null); - const envEditorRef = useRef(null); - const consoleRef = useRef(null); - - const handleScriptEditorDidMount: OnMount = useCallback(editor => { - scriptEditorRef.current = editor; - }, []); - - const handleEnvEditorDidMount: OnMount = useCallback(editor => { - envEditorRef.current = editor; - }, []); - - const handleMonacoEditorBeforeMount = useCallback((monaco: Monaco) => { - // Configure JavaScript defaults for TypeScript validation - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: false, - noSyntaxValidation: false, - noSuggestionDiagnostics: false, - diagnosticCodesToIgnore: [], // Can specify codes to ignore - }); - - // Enable modern JavaScript features and strict checks - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - target: monaco.languages.typescript.ScriptTarget.ESNext, - allowNonTsExtensions: true, - moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, - module: monaco.languages.typescript.ModuleKind.ESNext, - noEmit: true, - lib: ['es2021', 'dom'], - strict: true, - noUnusedLocals: true, - noUnusedParameters: true, - noImplicitReturns: true, - noFallthroughCasesInSwitch: true, - }); - - // Add custom typings for globalThis - monaco.languages.typescript.javascriptDefaults.addExtraLib( - ` - ${labApiDefinitionRaw} - declare const lab: LabAPI; - `, - 'global.d.ts', - ); - }, []); - - const handleSubmit = useCallback(() => { - onContentChange(scriptEditorRef.current?.getValue() ?? ''); - onEnvValueChange(envEditorRef.current?.getValue() ?? ''); - toggle(); - }, []); - - useEffect(() => { - const consoleEl = consoleRef.current; - consoleEl?.scroll({ top: consoleEl.scrollHeight, behavior: 'smooth' }); - }, [logs]); - - return ( - { - if (!open) { - abortExecution(); - } - toggle(); - }} - > - { - // prevent pressing escape in monaco to close the modal - if (ev.target instanceof HTMLTextAreaElement) { - ev.preventDefault(); - } - }} - > - - Edit your Preflight Script - - This script will run in each user's browser and be stored in plain text on our servers. - Don't share any secrets here. -
- All team members can view the script and toggle it off when they need to. - - -
-
-
- - Script Editor - - JavaScript - - - -
- -
-
-
- Console Output - -
-
- {logs.map((log, index) => ( - - ))} -
- - Environment Variables - - JSON - - - onEnvValueChange(value ?? '')} - onMount={handleEnvEditorDidMount} - {...monacoProps.env} - options={{ - ...monacoProps.env.options, - wordWrap: 'wordWrapColumn', - }} - wrapperProps={{ - ['data-cy']: 'env-editor', - }} - /> -
-
- -

- - Changes made to this Preflight Script will apply to all users on your team using this - target. -

- - -
- - - ); -} - -const LOG_COLORS = { - error: 'text-red-400', - info: 'text-emerald-400', - warn: 'text-yellow-400', - log: 'text-gray-400', -}; - -export function LogLine({ log }: { log: LogRecord }) { - if ('type' in log && log.type === 'separator') { - return
; - } - - if ('level' in log && log.level in LOG_COLORS) { - return ( -
- {log.level}: {log.message} - {log.line && log.column ? ` (${log.line}:${log.column})` : ''} -
- ); - } - - captureException(new Error('Unexpected log type in Preflight Script output'), { - extra: { log }, - }); - return null; -} diff --git a/packages/web/app/src/lib/preflight/index.ts b/packages/web/app/src/lib/preflight/index.ts new file mode 100644 index 0000000000..1b6e6dbb8e --- /dev/null +++ b/packages/web/app/src/lib/preflight/index.ts @@ -0,0 +1,3 @@ +export * from './graphiql-plugin'; +export * from './shared-types'; +export * from './components/LogLine'; diff --git a/packages/web/app/src/lib/preflight/shared-types.ts b/packages/web/app/src/lib/preflight/shared-types.ts index 25d63738e8..90a6b87078 100644 --- a/packages/web/app/src/lib/preflight/shared-types.ts +++ b/packages/web/app/src/lib/preflight/shared-types.ts @@ -2,6 +2,14 @@ import { Kit } from '../kit'; +// todo stop using enums +export const enum PreflightWorkerState { + running, + ready, +} + +export type LogRecord = LogMessage | { type: 'separator' }; + type _MessageEvent = MessageEvent; export type LogMessage = { diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 1ee90850e1..6989c467dd 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -42,7 +42,7 @@ import { PreflightProvider, PreflightResultData, usePreflight, -} from '@/lib/preflight/graphiql-plugin'; +} from '@/lib/preflight'; import { cn } from '@/lib/utils'; import { explorerPlugin } from '@graphiql/plugin-explorer'; import { From bae2af3065b019e6a4798b2c4be13b9e11c89fa3 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 11 Feb 2025 09:04:45 -0500 Subject: [PATCH 12/21] just refactor --- cypress/e2e/laboratory/preflight.cy.ts | 10 -- .../preflight/components/PreflightModal.tsx | 153 +++--------------- .../lib/preflight/components/ScriptEditor.tsx | 13 ++ .../app/src/lib/preflight/graphiql-plugin.tsx | 32 ++-- 4 files changed, 53 insertions(+), 155 deletions(-) diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 3e7d1b8c0e..f9e472e9e7 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -50,16 +50,6 @@ describe('Preflight Modal', () => { cyp.setEnvironmentEditorContent(env); }); - it('code is validated with TypeScript', () => { - const tsErrorMessage = "Type 'string' is not assignable to type 'number'."; - const script = 'let a = 1; a = ""'; - cyp.setEditorContent(script); - // Hack: Seemingly only way to reliably interact with the monaco text area from Cypress. - cy.wait(1000); - cy.dataCy(s.modal.editorCy).find('textarea').focus().realPress(['Alt', 'F8']); - cy.contains(tsErrorMessage); - }); - it('save script and environment variables when submitting', () => { cyp.setEditorContent(script); cy.dataCy('preflight-modal-submit').click(); diff --git a/packages/web/app/src/lib/preflight/components/PreflightModal.tsx b/packages/web/app/src/lib/preflight/components/PreflightModal.tsx index 887d65d43a..9c259a6b8e 100644 --- a/packages/web/app/src/lib/preflight/components/PreflightModal.tsx +++ b/packages/web/app/src/lib/preflight/components/PreflightModal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import type { editor } from 'monaco-editor'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -10,150 +10,51 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { MonacoEditorReact } from '@/lib/MonacoEditorReact'; import { Cross2Icon, InfoCircledIcon, TriangleRightIcon } from '@radix-ui/react-icons'; -import labApiDefinitionRaw from '../lab-api-declaration?raw'; import { LogRecord, PreflightWorkerState } from '../shared-types'; import { EditorTitle } from './EditorTitle'; import { EnvironmentEditor } from './EnvironmentEditor'; import { LogLine } from './LogLine'; import { ScriptEditor } from './ScriptEditor'; +export interface SaveResult { + scriptEditorValue: string; + environmentEditorValue: string; +} + export function PreflightModal({ isOpen, toggle, - content, + onSave, state, execute, abortExecution, logs, clearLogs, - onContentChange, - envValue, - onEnvValueChange, + scriptEditorValue: scriptEditorValueInit, + environmentEditorValue: environmentEditorValueInit, }: { + onSave?: (values: SaveResult) => void; isOpen: boolean; toggle: () => void; - content?: string; state: PreflightWorkerState; execute: (script: string) => void; abortExecution: () => void; logs: Array; clearLogs: () => void; - onContentChange: (value: string) => void; - envValue: string; - onEnvValueChange: (value: string) => void; + scriptEditorValue?: string; + environmentEditorValue?: string; }) { - const scriptEditorRef = useRef(null); - const envEditorRef = useRef(null); + const [scriptEditorValue, setScriptEditorValue] = useState(scriptEditorValueInit ?? ''); + const [environmentEditorValue, setEnvironmentEditorValue] = useState( + environmentEditorValueInit ?? '', + ); const consoleRef = useRef(null); - - const handleScriptEditorDidMount: MonacoEditorReact.OnMount = useCallback(editor => { - scriptEditorRef.current = editor; - }, []); - - const handleEnvEditorDidMount: MonacoEditorReact.OnMount = useCallback(editor => { - envEditorRef.current = editor; - }, []); - - const handleMonacoEditorBeforeMount = useCallback((monaco: MonacoEditorReact.Monaco) => { - // Configure JavaScript defaults for TypeScript validation - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: false, - noSyntaxValidation: false, - noSuggestionDiagnostics: false, - diagnosticCodesToIgnore: [], // Can specify codes to ignore - }); - // monaco.languages.typescript. - - // Enable modern JavaScript features and strict checks - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - allowNonTsExtensions: true, - allowJs: true, - checkJs: true, - // noEmit: true, - target: monaco.languages.typescript.ScriptTarget.ES2020, - // moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, - // module: monaco.languages.typescript.ModuleKind.ESNext, - lib: [], - // types: [], - strict: true, - noUnusedLocals: true, - noUnusedParameters: true, - noImplicitReturns: true, - noFallthroughCasesInSwitch: true, + const handleSave = useCallback(() => { + onSave?.({ + scriptEditorValue: scriptEditorValue, + environmentEditorValue: environmentEditorValue, }); - - // Add custom typings for globalThis - monaco.languages.typescript.javascriptDefaults.addExtraLib( - ` - /// - - ${labApiDefinitionRaw} - declare const lab: LabAPI; - - - // ------------------------------------------------------------------------------------------------ - // The following declarations are taken from: - // https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts - // ------------------------------------------------------------------------------------------------ - - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */ - interface Console { - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/assert_static) */ - assert(condition?: boolean, ...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) */ - clear(): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */ - count(label?: string): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) */ - countReset(label?: string): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */ - debug(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) */ - dir(item?: any, options?: any): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) */ - dirxml(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) */ - error(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) */ - group(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) */ - groupCollapsed(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) */ - groupEnd(): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */ - info(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) */ - log(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) */ - table(tabularData?: any, properties?: string[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) */ - time(label?: string): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) */ - timeEnd(label?: string): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) */ - timeLog(label?: string, ...data: any[]): void; - timeStamp(label?: string): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) */ - trace(...data: any[]): void; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) */ - warn(...data: any[]): void; - } - declare const console: Console; - - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ - declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; - - type TimerHandler = string | Function; - `, - 'global.d.ts', - ); - }, []); - - const handleSubmit = useCallback(() => { - onContentChange(scriptEditorRef.current?.getValue() ?? ''); - onEnvValueChange(envEditorRef.current?.getValue() ?? ''); toggle(); }, []); @@ -209,7 +110,7 @@ export function PreflightModal({ return; } - execute(scriptEditorRef.current?.getValue() ?? ''); + execute(scriptEditorValue); }} data-cy="run-preflight" > @@ -228,9 +129,8 @@ export function PreflightModal({ setScriptEditorValue(value ?? '')} options={{ wordWrap: 'wordWrapColumn', }} @@ -269,9 +169,8 @@ export function PreflightModal({ onEnvValueChange(value ?? '')} - onMount={handleEnvEditorDidMount} + value={environmentEditorValue} + onChange={value => setEnvironmentEditorValue(value ?? '')} options={{ wordWrap: 'wordWrapColumn', }} @@ -293,7 +192,7 @@ export function PreflightModal({