From e3a3a07a90ad4c73fc8c074163725c1134a40c4c Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 5 Feb 2025 15:47:03 -0500 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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 3b38db800437214ce1ce25a80809c97c6306c8b5 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 09:25:52 -0500 Subject: [PATCH 12/23] undo refactor --- .../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 | 321 +++++++++++++++++- .../web/app/src/lib/preflight/shared-types.ts | 8 - .../web/app/src/pages/target-laboratory.tsx | 2 +- 9 files changed, 308 insertions(+), 439 deletions(-) delete mode 100644 packages/web/app/src/lib/preflight/components/EditorTitle.tsx delete mode 100644 packages/web/app/src/lib/preflight/components/EnvironmentEditor.tsx delete mode 100644 packages/web/app/src/lib/preflight/components/LogLine.tsx delete mode 100644 packages/web/app/src/lib/preflight/components/PreflightModal.tsx delete mode 100644 packages/web/app/src/lib/preflight/components/ScriptEditor.tsx delete mode 100644 packages/web/app/src/lib/preflight/components/_defaultEditorProps.ts diff --git a/packages/web/app/src/lib/preflight/components/EditorTitle.tsx b/packages/web/app/src/lib/preflight/components/EditorTitle.tsx deleted file mode 100644 index 3525462904..0000000000 --- a/packages/web/app/src/lib/preflight/components/EditorTitle.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 05f3d24626..0000000000 --- a/packages/web/app/src/lib/preflight/components/EnvironmentEditor.tsx +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 54c46252a9..0000000000 --- a/packages/web/app/src/lib/preflight/components/LogLine.tsx +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 887d65d43a..0000000000 --- a/packages/web/app/src/lib/preflight/components/PreflightModal.tsx +++ /dev/null @@ -1,305 +0,0 @@ -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 deleted file mode 100644 index a9f26941ff..0000000000 --- a/packages/web/app/src/lib/preflight/components/ScriptEditor.tsx +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 5f48d77eec..0000000000 --- a/packages/web/app/src/lib/preflight/components/_defaultEditorProps.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 0020e3f673..30fb4f595a 100644 --- a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx @@ -10,34 +10,36 @@ 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 { Pencil1Icon } from '@radix-ui/react-icons'; +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 { useParams } from '@tanstack/react-router'; import { Kit } from '../kit'; import { cn } from '../utils'; -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'; +import labApiDefinitionRaw from './lab-api-declaration?raw'; +import { IFrameEvents, LogMessage } 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, + }, + }, +} satisfies Record<'script' | 'env', ComponentPropsWithoutRef>; + const UpdatePreflightScriptMutation = graphql(` mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) { updatePreflightScript(input: $input) { @@ -86,6 +137,13 @@ const PreflightScript_TargetFragment = graphql(` } `); +export type LogRecord = LogMessage | { type: 'separator' }; + +export const enum PreflightWorkerState { + running, + ready, +} + export function usePreflight(args: { target: FragmentType | null; }) { @@ -500,14 +558,16 @@ 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', @@ -539,3 +600,235 @@ 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) => { + // 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/shared-types.ts b/packages/web/app/src/lib/preflight/shared-types.ts index 90a6b87078..25d63738e8 100644 --- a/packages/web/app/src/lib/preflight/shared-types.ts +++ b/packages/web/app/src/lib/preflight/shared-types.ts @@ -2,14 +2,6 @@ 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 6989c467dd..1ee90850e1 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'; +} from '@/lib/preflight/graphiql-plugin'; import { cn } from '@/lib/utils'; import { explorerPlugin } from '@graphiql/plugin-explorer'; import { From c420094fb91a67061bcef49bea8a9fba8c3d9c6d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 09:27:26 -0500 Subject: [PATCH 13/23] undo refactor --- packages/web/app/src/lib/preflight/index.ts | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 packages/web/app/src/lib/preflight/index.ts diff --git a/packages/web/app/src/lib/preflight/index.ts b/packages/web/app/src/lib/preflight/index.ts deleted file mode 100644 index 1b6e6dbb8e..0000000000 --- a/packages/web/app/src/lib/preflight/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './graphiql-plugin'; -export * from './shared-types'; -export * from './components/LogLine'; From 26cb47a5b9eb63520d1116f64ac47a903ceedf2d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 09:48:20 -0500 Subject: [PATCH 14/23] ts validation --- .../app/src/lib/preflight/graphiql-plugin.tsx | 19 +++++++++++++++++++ 1 file changed, 19 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..d77f4798ee 100644 --- a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx @@ -639,6 +639,25 @@ function PreflightModal({ }, []); const handleMonacoEditorBeforeMount = useCallback((monaco: Monaco) => { + // Setup validation of JavaScript code. + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false, + noSuggestionDiagnostics: false, + diagnosticCodesToIgnore: [], + }); + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + allowNonTsExtensions: true, + allowJs: true, + checkJs: true, + target: monaco.languages.typescript.ScriptTarget.ES2020, + lib: ['webworker'], + strict: true, + noUnusedLocals: true, + noUnusedParameters: true, + noImplicitReturns: true, + noFallthroughCasesInSwitch: true, + }); // Add custom typings for globalThis monaco.languages.typescript.javascriptDefaults.addExtraLib( ` From 8d82086bf32e1e742493551133293e7ce7fdb4c1 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 09:52:23 -0500 Subject: [PATCH 15/23] remove refactor --- packages/web/app/src/lib/MonacoEditorReact/index.ts | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 packages/web/app/src/lib/MonacoEditorReact/index.ts diff --git a/packages/web/app/src/lib/MonacoEditorReact/index.ts b/packages/web/app/src/lib/MonacoEditorReact/index.ts deleted file mode 100644 index 81f3ecce7c..0000000000 --- a/packages/web/app/src/lib/MonacoEditorReact/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as MonacoEditorReact from '@monaco-editor/react'; - -export { MonacoEditorReact }; From b734ade0e65b2855420ce17c75d2633e507e5165 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 09:54:03 -0500 Subject: [PATCH 16/23] try feedback change --- cypress/e2e/laboratory/{_cy.ts => __cypress__.ts} | 0 cypress/e2e/laboratory/collections.cy.ts | 2 +- cypress/e2e/laboratory/preflight.cy.ts | 2 +- cypress/e2e/laboratory/tabs.cy.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename cypress/e2e/laboratory/{_cy.ts => __cypress__.ts} (100%) diff --git a/cypress/e2e/laboratory/_cy.ts b/cypress/e2e/laboratory/__cypress__.ts similarity index 100% rename from cypress/e2e/laboratory/_cy.ts rename to cypress/e2e/laboratory/__cypress__.ts diff --git a/cypress/e2e/laboratory/collections.cy.ts b/cypress/e2e/laboratory/collections.cy.ts index 086aa410b5..b8bea98548 100644 --- a/cypress/e2e/laboratory/collections.cy.ts +++ b/cypress/e2e/laboratory/collections.cy.ts @@ -1,4 +1,4 @@ -import { cyLaboratory } from './_cy'; +import { cyLaboratory } from './__cypress__'; beforeEach(() => { cy.clearAllLocalStorage().then(() => { diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 3e7d1b8c0e..76989a89f1 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -1,5 +1,5 @@ import { dedent } from '../../support/dedent'; -import { cyLaboratory } from './_cy'; +import { cyLaboratory } from './__cypress__'; const s = cyLaboratory.preflight.selectors; diff --git a/cypress/e2e/laboratory/tabs.cy.ts b/cypress/e2e/laboratory/tabs.cy.ts index ea063e0c63..43da53c44a 100644 --- a/cypress/e2e/laboratory/tabs.cy.ts +++ b/cypress/e2e/laboratory/tabs.cy.ts @@ -1,4 +1,4 @@ -import { cyLaboratory } from './_cy'; +import { cyLaboratory } from './__cypress__'; beforeEach(() => { cy.clearAllLocalStorage().then(() => { From 80eb5721953e424edc2a538db6e6ca82f43ce90f Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 10:13:44 -0500 Subject: [PATCH 17/23] feedback --- cypress/e2e/laboratory/__cypress__.ts | 8 +- cypress/e2e/laboratory/preflight.cy.ts | 101 +++++++++++++------------ cypress/support/monaco.ts | 45 +++++++---- 3 files changed, 86 insertions(+), 68 deletions(-) diff --git a/cypress/e2e/laboratory/__cypress__.ts b/cypress/e2e/laboratory/__cypress__.ts index 61ce189fd3..470e095ef3 100644 --- a/cypress/e2e/laboratory/__cypress__.ts +++ b/cypress/e2e/laboratory/__cypress__.ts @@ -1,4 +1,4 @@ -import { setMonacoEditorContents } from '../../support/monaco'; +import { cyMonaco } from '../../support/monaco'; export namespace cyLaboratory { /** @@ -81,15 +81,15 @@ export namespace cyLaboratory { /** * Sets the content of the preflight editor */ - export const setEditorContent = (value: string) => { - setMonacoEditorContents(selectors.modal.editorCy, value); + export const setScriptEditorContent = (value: string) => { + cyMonaco.setContent(selectors.modal.editorCy, value); }; /** * Sets the content of the variables editor */ export const setEnvironmentEditorContent = (value: string) => { - setMonacoEditorContents(selectors.modal.variablesEditorCy, value); + cyMonaco.setContent(selectors.modal.variablesEditorCy, value); }; } } diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 76989a89f1..5782232085 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -1,9 +1,10 @@ import { dedent } from '../../support/dedent'; +import { cyMonaco } from '../../support/monaco'; import { cyLaboratory } from './__cypress__'; -const s = cyLaboratory.preflight.selectors; +const selectors = cyLaboratory.preflight.selectors; -const cyp = cyLaboratory.preflight; +const cyPreflight = cyLaboratory.preflight; const data: { slug: string } = { slug: '', @@ -15,7 +16,7 @@ beforeEach(() => { cy.setCookie('sRefreshToken', refreshToken); data.slug = slug; cy.visit(`/${slug}/laboratory`); - cy.get(s.buttonGraphiQLPreflight).click(); + cy.get(selectors.buttonGraphiQLPreflight).click(); }); }); }); @@ -25,7 +26,7 @@ describe('Preflight Tab', () => { it('regression: loads even if local storage is set to {}', () => { window.localStorage.setItem('hive:laboratory:environment', '{}'); cy.visit(`/${data.slug}/laboratory`); - cy.get(s.buttonGraphiQLPreflight).click(); + cy.get(selectors.buttonGraphiQLPreflight).click(); }); it('mini script editor is read only', () => { cy.dataCy('toggle-preflight').click(); @@ -47,21 +48,21 @@ describe('Preflight Modal', () => { beforeEach(() => { cy.dataCy('preflight-modal-button').click(); - cyp.setEnvironmentEditorContent(env); + cyPreflight.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('script is validated with TypeScript', () => { + cyPreflight.setScriptEditorContent('let a = 1; a = ""'); + cyMonaco.nextProblemContains(selectors.modal.editorCy, "Type 'string' is not assignable to type 'number'."); // prettier-ignore + }); + + it('script cannot have TypeScript syntax', () => { + cyPreflight.setScriptEditorContent('let a:number = 1'); + cyMonaco.nextProblemContains(selectors.modal.editorCy, 'Type annotations can only be used in TypeScript files.'); // prettier-ignore }); it('save script and environment variables when submitting', () => { - cyp.setEditorContent(script); + cyPreflight.setScriptEditorContent(script); cy.dataCy('preflight-modal-submit').click(); cy.dataCy('env-editor-mini').should('have.text', env); cy.dataCy('toggle-preflight').click(); @@ -73,11 +74,11 @@ describe('Preflight Modal', () => { }); it('logs show console/error information', () => { - cyp.setEditorContent(script); + cyPreflight.setScriptEditorContent(script); cy.dataCy('run-preflight').click(); cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); - cyp.setEditorContent( + cyPreflight.setScriptEditorContent( `console.info(1) console.warn(true) console.error('Fatal') @@ -95,12 +96,12 @@ throw new TypeError('Test')`, }); it('prompt and pass the awaited response', () => { - cyp.setEditorContent(script); + cyPreflight.setScriptEditorContent(script); cy.dataCy('run-preflight').click(); cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); - cyp.setEditorContent( + cyPreflight.setScriptEditorContent( dedent` const username = await lab.prompt('Enter your username'); console.info(username); @@ -123,12 +124,12 @@ throw new TypeError('Test')`, }); it('prompt and cancel', () => { - cyp.setEditorContent(script); + cyPreflight.setScriptEditorContent(script); cy.dataCy('run-preflight').click(); cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); - cyp.setEditorContent( + cyPreflight.setScriptEditorContent( dedent` const username = await lab.prompt('Enter your username'); console.info(username); @@ -151,7 +152,7 @@ throw new TypeError('Test')`, }); it('script execution updates environment variables', () => { - cyp.setEditorContent(`lab.environment.set('my-test', "TROLOLOL")`); + cyPreflight.setScriptEditorContent(`lab.environment.set('my-test', "TROLOLOL")`); cy.dataCy('run-preflight').click(); cy.dataCy('env-editor').should( @@ -162,7 +163,7 @@ throw new TypeError('Test')`, }); it('`crypto-js` can be used for generating hashes', () => { - cyp.setEditorContent('console.log(lab.CryptoJS.SHA256("🐝"))'); + cyPreflight.setScriptEditorContent('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( @@ -172,13 +173,13 @@ throw new TypeError('Test')`, }); it('scripts can not use `eval`', () => { - cyp.setEditorContent('eval()'); + cyPreflight.setScriptEditorContent('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', () => { - cyp.setEditorContent('🐝'); + cyPreflight.setScriptEditorContent('🐝'); cy.dataCy('preflight-modal-submit').click(); cy.get('body').contains("[1:1]: Illegal character '}"); }); @@ -190,13 +191,15 @@ describe('Execution', () => { const preflightHeaders = { foo: 'bar', }; - 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(); + cy.dataCy(selectors.buttonToggleCy).click(); + cy.dataCy(selectors.buttonModalCy).click(); + cyPreflight.setScriptEditorContent( + `lab.request.headers.append('foo', '${preflightHeaders.foo}')`, + ); + cy.dataCy(selectors.modal.buttonSubmitCy).click(); // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); - cy.get(s.graphiql.buttonExecute).click(); + cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@request'); }); @@ -208,19 +211,21 @@ describe('Execution', () => { accept: 'application/json, multipart/mixed', }; cy.intercept({ headers: baseHeaders }).as('integrityCheck'); - cy.get(s.graphiql.buttonExecute).click(); + cy.get(selectors.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(s.buttonToggleCy).click(); - cy.dataCy(s.buttonModalCy).click(); - cyp.setEditorContent(`lab.request.headers.append('accept', '${preflightHeaders.accept}')`); - cy.dataCy(s.modal.buttonSubmitCy).click(); + cy.dataCy(selectors.buttonToggleCy).click(); + cy.dataCy(selectors.buttonModalCy).click(); + cyPreflight.setScriptEditorContent( + `lab.request.headers.append('accept', '${preflightHeaders.accept}')`, + ); + cy.dataCy(selectors.modal.buttonSubmitCy).click(); // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); - cy.get(s.graphiql.buttonExecute).click(); + cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@request'); }); @@ -230,8 +235,8 @@ describe('Execution', () => { const staticHeaders = { foo_static: barEnVarInterpolation, }; - cy.get(s.buttonHeaders).click(); - cy.get(s.headersEditor.textArea).type(JSON.stringify(staticHeaders), { + cy.get(selectors.buttonHeaders).click(); + cy.get(selectors.headersEditor.textArea).type(JSON.stringify(staticHeaders), { force: true, parseSpecialCharSequences: false, }); @@ -242,13 +247,13 @@ describe('Execution', () => { const preflightHeaders = { foo_preflight: barEnVarInterpolation, }; - cy.dataCy(s.buttonToggleCy).click(); - cy.dataCy(s.buttonModalCy).click(); - cyp.setEditorContent(` + cy.dataCy(selectors.buttonToggleCy).click(); + cy.dataCy(selectors.buttonModalCy).click(); + cyPreflight.setScriptEditorContent(` lab.environment.set('bar', '${environmentVariables.bar}') lab.request.headers.append('foo_preflight', '${preflightHeaders.foo_preflight}') `); - cy.dataCy(s.modal.buttonSubmitCy).click(); + cy.dataCy(selectors.modal.buttonSubmitCy).click(); // Run GraphiQL cy.intercept({ headers: { @@ -256,7 +261,7 @@ describe('Execution', () => { foo_static: environmentVariables.bar, }, }).as('request'); - cy.get(s.graphiql.buttonExecute).click(); + cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@request'); }); @@ -298,7 +303,7 @@ describe('Execution', () => { }, ); cy.dataCy('preflight-modal-button').click(); - cyp.setEditorContent(`lab.environment.set('foo', '92')`); + cyPreflight.setScriptEditorContent(`lab.environment.set('foo', '92')`); cy.dataCy('preflight-modal-submit').click(); cy.intercept({ @@ -325,7 +330,7 @@ describe('Execution', () => { ); cy.dataCy('preflight-modal-button').click(); - cyp.setEditorContent( + cyPreflight.setScriptEditorContent( dedent` const username = await lab.prompt('Enter your username'); lab.environment.set('username', username); @@ -357,8 +362,8 @@ describe('Execution', () => { }, ); cy.dataCy('preflight-modal-button').click(); - cyp.setEditorContent(`lab.environment.set('foo', 92)`); - cyp.setEnvironmentEditorContent(`{"foo":10}`); + cyPreflight.setScriptEditorContent(`lab.environment.set('foo', 92)`); + cyPreflight.setEnvironmentEditorContent(`{"foo":10}`); cy.dataCy('preflight-modal-submit').click(); @@ -376,7 +381,7 @@ describe('Execution', () => { cy.dataCy('toggle-preflight').click(); cy.dataCy('preflight-modal-button').click(); - cyp.setEditorContent( + cyPreflight.setScriptEditorContent( dedent` console.info(1) console.warn(true) @@ -420,7 +425,7 @@ describe('Execution', () => { cy.dataCy('toggle-preflight').click(); cy.dataCy('preflight-modal-button').click(); - cyp.setEditorContent( + cyPreflight.setScriptEditorContent( dedent` console.info(1) console.warn(true) diff --git a/cypress/support/monaco.ts b/cypress/support/monaco.ts index 7636c401e7..35ddcc6ab6 100644 --- a/cypress/support/monaco.ts +++ b/cypress/support/monaco.ts @@ -1,20 +1,33 @@ 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: Window & typeof globalThis & { monaco: typeof Monaco }) => { - // First, check if monaco is available on the main window - const editor = win.monaco.editor - .getEditors() - .find(e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorCyName); +export namespace cyMonaco { + /** Helper function for setting the text within a monaco editor as typing manually results in flaky tests */ + export function setContent(editorCyName: string, text: string) { + // wait for textarea appearing which indicates monaco is loaded + cy.dataCy(editorCyName).find('textarea'); + cy.window().then((win: Window & typeof globalThis & { monaco: typeof Monaco }) => { + // First, check if monaco is available on the main window + const editor = win.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]'); - } - }); + // If Monaco instance is found + if (editor) { + editor.setValue(text); + } else { + throw new Error('Monaco editor not found on the window or frames[0]'); + } + }); + } + + export function goToNextProblem(editorCyName: string) { + cy.dataCy(editorCyName).find('textarea').focus().realPress(['Alt', 'F8']); + } + + export function nextProblemContains(editorCyName: string, problem: string, waitMs = 1000) { + // Hack: Seemingly only way to reliably interact with the monaco text area from Cypress. + if (waitMs) cy.wait(waitMs); + goToNextProblem(editorCyName); + cy.contains(problem); + } } From 7c25002d64572ec5453bd21fa0ed79618f31ae1d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 10:38:01 -0500 Subject: [PATCH 18/23] add changeset --- .changeset/funny-poets-hang.md | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .changeset/funny-poets-hang.md diff --git a/.changeset/funny-poets-hang.md b/.changeset/funny-poets-hang.md new file mode 100644 index 0000000000..2bf257c28e --- /dev/null +++ b/.changeset/funny-poets-hang.md @@ -0,0 +1,36 @@ +--- +'hive': minor +--- + +Laboratory Preflight now validates your script with TypeScript. Also, the `WebWorker` runtime types are applied giving you confidence about what globals are available to you in your script. + +## Backwards Incompatible Notes + +This change is backwards incompatible in the sense that invalid or problematic Script code which would have previously not statically errored will now. However at this time we do not prevent script saving because of static type errors. Therefore your workflow should only at worst be visually impacted. + +## About WebWorker Runtime & Types + +To learn more about what the WebWorker runtime and types are, you can review the following: + +1. https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API +2. https://www.typescriptlang.org/tsconfig/#lib (see "WebWorker") + + +## Leveraging TypeScript in JavaScript + +If you are not familiar with TypeScript, here is a tip for you if you find yourself with a TypeScript error that you cannot or do not want to fix. You can silence them by using comments: + +```js +let a = 1; +let b = ''; +// @ts-ignore +a = b; +// @ts-expect-error +a = b; +``` + +The advantage of `@ts-expect-error` is that if there is no error to ignore, then the comment itself becomes an error whereas `@ts-ignore` sits there quietly whether it has effect or not. + +There is more you can do with TypeScript in JavaScript, such as providing type annotations via JSDoc. Learn more about it all here: + +https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html From 0aa198e694494fa73fc30e9afef01a069715807d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 11:07:09 -0500 Subject: [PATCH 19/23] no cy selectors --- .changeset/funny-poets-hang.md | 2 +- cypress/e2e/laboratory/__cypress__.ts | 21 +++++++---------- cypress/e2e/laboratory/preflight.cy.ts | 31 +++++++++++++++++--------- cypress/support/monaco.ts | 16 +++++++------ 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/.changeset/funny-poets-hang.md b/.changeset/funny-poets-hang.md index 2bf257c28e..f9172dbca4 100644 --- a/.changeset/funny-poets-hang.md +++ b/.changeset/funny-poets-hang.md @@ -29,7 +29,7 @@ a = b; a = b; ``` -The advantage of `@ts-expect-error` is that if there is no error to ignore, then the comment itself becomes an error whereas `@ts-ignore` sits there quietly whether it has effect or not. +The advantage of `@ts-expect-error` is that if there is no error to ignore, then the comment itself becomes an error whereas `@ts-ignore` sits there quietly whether it has an effect or not. There is more you can do with TypeScript in JavaScript, such as providing type annotations via JSDoc. Learn more about it all here: diff --git a/cypress/e2e/laboratory/__cypress__.ts b/cypress/e2e/laboratory/__cypress__.ts index 470e095ef3..360d65b824 100644 --- a/cypress/e2e/laboratory/__cypress__.ts +++ b/cypress/e2e/laboratory/__cypress__.ts @@ -62,8 +62,8 @@ export namespace cyLaboratory { export namespace preflight { export const selectors = { buttonGraphiQLPreflight: '[aria-label*="Preflight Script"]', - buttonModalCy: 'preflight-modal-button', - buttonToggleCy: 'toggle-preflight', + buttonModal: '[data-cy="preflight-modal-button"]', + buttonToggle: '[data-cy="toggle-preflight"]', buttonHeaders: '[data-name="headers"]', headersEditor: { textArea: '.graphiql-editor-tool .graphiql-editor:last-child textarea', @@ -73,23 +73,18 @@ export namespace cyLaboratory { }, modal: { - buttonSubmitCy: 'preflight-modal-submit', - editorCy: 'preflight-editor', - variablesEditorCy: 'env-editor', + buttonSubmit: '[data-cy="preflight-modal-submit"]', + scriptEditor: '[data-cy="preflight-editor"]', + variablesEditor: '[data-cy="env-editor"]', }, }; - /** - * Sets the content of the preflight editor - */ + export const setScriptEditorContent = (value: string) => { - cyMonaco.setContent(selectors.modal.editorCy, value); + cyMonaco.setContent(selectors.modal.scriptEditor, value); }; - /** - * Sets the content of the variables editor - */ export const setEnvironmentEditorContent = (value: string) => { - cyMonaco.setContent(selectors.modal.variablesEditorCy, value); + cyMonaco.setContent(selectors.modal.variablesEditor, value); }; } } diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 5782232085..3db5a659ea 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -53,12 +53,21 @@ describe('Preflight Modal', () => { it('script is validated with TypeScript', () => { cyPreflight.setScriptEditorContent('let a = 1; a = ""'); - cyMonaco.nextProblemContains(selectors.modal.editorCy, "Type 'string' is not assignable to type 'number'."); // prettier-ignore + cyMonaco.nextProblemContains(selectors.modal.scriptEditor, "Type 'string' is not assignable to type 'number'."); // prettier-ignore }); it('script cannot have TypeScript syntax', () => { cyPreflight.setScriptEditorContent('let a:number = 1'); - cyMonaco.nextProblemContains(selectors.modal.editorCy, 'Type annotations can only be used in TypeScript files.'); // prettier-ignore + cyMonaco.nextProblemContains(selectors.modal.scriptEditor, 'Type annotations can only be used in TypeScript files.'); // prettier-ignore + }); + + it('regression: saving and re-opening clears previous validation state', () => { + cyPreflight.setScriptEditorContent('const a = 1'); + cy.get(selectors.modal.buttonSubmit).click(); + cy.get(selectors.buttonModal).click(); + cy.wait(1000); + cyMonaco.goToNextProblem(selectors.modal.scriptEditor); + cy.contains('Cannot redeclare block-scoped variable').should('not.exist'); }); it('save script and environment variables when submitting', () => { @@ -191,12 +200,12 @@ describe('Execution', () => { const preflightHeaders = { foo: 'bar', }; - cy.dataCy(selectors.buttonToggleCy).click(); - cy.dataCy(selectors.buttonModalCy).click(); + cy.get(selectors.buttonToggle).click(); + cy.get(selectors.buttonModal).click(); cyPreflight.setScriptEditorContent( `lab.request.headers.append('foo', '${preflightHeaders.foo}')`, ); - cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.get(selectors.modal.buttonSubmit).click(); // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); cy.get(selectors.graphiql.buttonExecute).click(); @@ -217,12 +226,12 @@ describe('Execution', () => { 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(); + cy.get(selectors.buttonToggle).click(); + cy.get(selectors.buttonModal).click(); cyPreflight.setScriptEditorContent( `lab.request.headers.append('accept', '${preflightHeaders.accept}')`, ); - cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.get(selectors.modal.buttonSubmit).click(); // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); cy.get(selectors.graphiql.buttonExecute).click(); @@ -247,13 +256,13 @@ describe('Execution', () => { const preflightHeaders = { foo_preflight: barEnVarInterpolation, }; - cy.dataCy(selectors.buttonToggleCy).click(); - cy.dataCy(selectors.buttonModalCy).click(); + cy.get(selectors.buttonToggle).click(); + cy.get(selectors.buttonModal).click(); cyPreflight.setScriptEditorContent(` lab.environment.set('bar', '${environmentVariables.bar}') lab.request.headers.append('foo_preflight', '${preflightHeaders.foo_preflight}') `); - cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.get(selectors.modal.buttonSubmit).click(); // Run GraphiQL cy.intercept({ headers: { diff --git a/cypress/support/monaco.ts b/cypress/support/monaco.ts index 35ddcc6ab6..6adbc204bd 100644 --- a/cypress/support/monaco.ts +++ b/cypress/support/monaco.ts @@ -2,14 +2,16 @@ import type * as Monaco from 'monaco-editor'; export namespace cyMonaco { /** Helper function for setting the text within a monaco editor as typing manually results in flaky tests */ - export function setContent(editorCyName: string, text: string) { + export function setContent(editorSelector: string, text: string) { // wait for textarea appearing which indicates monaco is loaded - cy.dataCy(editorCyName).find('textarea'); + cy.get(editorSelector).find('textarea'); cy.window().then((win: Window & typeof globalThis & { monaco: typeof Monaco }) => { // First, check if monaco is available on the main window const editor = win.monaco.editor .getEditors() - .find(e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorCyName); + .find( + e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorSelector, + ); // If Monaco instance is found if (editor) { @@ -20,14 +22,14 @@ export namespace cyMonaco { }); } - export function goToNextProblem(editorCyName: string) { - cy.dataCy(editorCyName).find('textarea').focus().realPress(['Alt', 'F8']); + export function goToNextProblem(editorSelector: string) { + cy.get(editorSelector).find('textarea').focus().realPress(['Alt', 'F8']); } - export function nextProblemContains(editorCyName: string, problem: string, waitMs = 1000) { + export function nextProblemContains(editorSelector: string, problem: string, waitMs = 1000) { // Hack: Seemingly only way to reliably interact with the monaco text area from Cypress. if (waitMs) cy.wait(waitMs); - goToNextProblem(editorCyName); + goToNextProblem(editorSelector); cy.contains(problem); } } From fe3f808893465b90f7364766609e424e9091502a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 12:14:35 -0500 Subject: [PATCH 20/23] fix --- cypress/e2e/laboratory/preflight.cy.ts | 3 +-- cypress/support/monaco.ts | 17 ++++++++--------- .../app/src/lib/preflight/graphiql-plugin.tsx | 7 +++++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 3db5a659ea..53a8c550bb 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -62,10 +62,9 @@ describe('Preflight Modal', () => { }); it('regression: saving and re-opening clears previous validation state', () => { - cyPreflight.setScriptEditorContent('const a = 1'); + cyPreflight.setScriptEditorContent('const a = 1; a'); cy.get(selectors.modal.buttonSubmit).click(); cy.get(selectors.buttonModal).click(); - cy.wait(1000); cyMonaco.goToNextProblem(selectors.modal.scriptEditor); cy.contains('Cannot redeclare block-scoped variable').should('not.exist'); }); diff --git a/cypress/support/monaco.ts b/cypress/support/monaco.ts index 6adbc204bd..76cf8869ae 100644 --- a/cypress/support/monaco.ts +++ b/cypress/support/monaco.ts @@ -7,11 +7,10 @@ export namespace cyMonaco { cy.get(editorSelector).find('textarea'); cy.window().then((win: Window & typeof globalThis & { monaco: typeof Monaco }) => { // First, check if monaco is available on the main window - const editor = win.monaco.editor - .getEditors() - .find( - e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorSelector, - ); + const editor = win.monaco.editor.getEditors().find(e => { + const parentElement = e.getContainerDomNode().parentElement; + return Cypress.$(parentElement).is(editorSelector); + }); // If Monaco instance is found if (editor) { @@ -22,14 +21,14 @@ export namespace cyMonaco { }); } - export function goToNextProblem(editorSelector: string) { + export function goToNextProblem(editorSelector: string, waitMs = 1000) { + // Hack: Seemingly only way to reliably interact with the monaco text area from Cypress. + if (waitMs) cy.wait(waitMs); cy.get(editorSelector).find('textarea').focus().realPress(['Alt', 'F8']); } export function nextProblemContains(editorSelector: string, problem: string, waitMs = 1000) { - // Hack: Seemingly only way to reliably interact with the monaco text area from Cypress. - if (waitMs) cy.wait(waitMs); - goToNextProblem(editorSelector); + goToNextProblem(editorSelector, waitMs); cy.contains(problem); } } diff --git a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx index d77f4798ee..a6be547377 100644 --- a/packages/web/app/src/lib/preflight/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight/graphiql-plugin.tsx @@ -657,6 +657,13 @@ function PreflightModal({ noUnusedParameters: true, noImplicitReturns: true, noFallthroughCasesInSwitch: true, + // This is a workaround. + // 3 = 'force' + // + // Problem: https://github.com/graphql-hive/console/pull/6476#issuecomment-2654056957 + // Solution: https://github.com/microsoft/monaco-editor/issues/2976#issuecomment-2334468503 + // Reference: https://www.typescriptlang.org/tsconfig/#moduleDetection + moduleDetection: 3, }); // Add custom typings for globalThis monaco.languages.typescript.javascriptDefaults.addExtraLib( From fb29bddaedb85b86dcdf0e6d360107beaf1db8b2 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 14:07:31 -0500 Subject: [PATCH 21/23] fixes --- cypress/e2e/laboratory/preflight.cy.ts | 2 +- cypress/support/e2e.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 53a8c550bb..929180b584 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -52,7 +52,7 @@ describe('Preflight Modal', () => { }); it('script is validated with TypeScript', () => { - cyPreflight.setScriptEditorContent('let a = 1; a = ""'); + cyPreflight.setScriptEditorContent('let a = 1; a; a = ""'); cyMonaco.nextProblemContains(selectors.modal.scriptEditor, "Type 'string' is not assignable to type 'number'."); // prettier-ignore }); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index bca30a348c..7a8c2de2bb 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -2,6 +2,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. +// eslint-disable-next-line import/no-extraneous-dependencies import 'cypress-real-events'; Cypress.on('uncaught:exception', (_err, _runnable) => { From 865a7bf03efd11ed18bd909e7c595f67a762b9a5 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 14:09:22 -0500 Subject: [PATCH 22/23] fixes --- cypress/e2e/laboratory/preflight.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/laboratory/preflight.cy.ts b/cypress/e2e/laboratory/preflight.cy.ts index 929180b584..5b635a86e2 100644 --- a/cypress/e2e/laboratory/preflight.cy.ts +++ b/cypress/e2e/laboratory/preflight.cy.ts @@ -57,7 +57,7 @@ describe('Preflight Modal', () => { }); it('script cannot have TypeScript syntax', () => { - cyPreflight.setScriptEditorContent('let a:number = 1'); + cyPreflight.setScriptEditorContent('const a:number = 1; a'); cyMonaco.nextProblemContains(selectors.modal.scriptEditor, 'Type annotations can only be used in TypeScript files.'); // prettier-ignore }); From 08b3640040230552a4bb431c4b3ec75cfb725211 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 12 Feb 2025 15:21:47 -0500 Subject: [PATCH 23/23] format --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 5190809761..7de07404b4 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run --browser chrome", "test:e2e:local": "CYPRESS_BASE_URL=http://localhost:3000 RUN_AGAINST_LOCAL_SERVICES=1 cypress open --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",