diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9fc760c5d..14a56cb2d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -23,6 +23,7 @@ module.exports = { 'install-k6.js', '.eslintrc.cjs', '**/__snapshots__/', + 'tests', ], plugins: [ 'import', diff --git a/.github/workflows/release-test-version.yml b/.github/workflows/release-test-version.yml index 2e4c7abea..5fc9bb35f 100644 --- a/.github/workflows/release-test-version.yml +++ b/.github/workflows/release-test-version.yml @@ -150,3 +150,8 @@ jobs: if: startsWith(matrix.platform, 'windows-') run: | del certificate.pfx + + - name: automated tests + if: startsWith(matrix.platform, 'macos-') + run: | + npm run flow-test diff --git a/.gitignore b/.gitignore index 12219ee8f..73693cc3b 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,6 @@ resources/win/x86_64/k6 # Sentry Config File .env.sentry-build-plugin + +# tests +test-results diff --git a/forge.config.ts b/forge.config.ts index f16aadc70..75ba9a71c 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -104,7 +104,7 @@ const config: ForgeConfig = { [FuseV1Options.RunAsNode]: false, [FuseV1Options.EnableCookieEncryption]: true, [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, - [FuseV1Options.EnableNodeCliInspectArguments]: false, + [FuseV1Options.EnableNodeCliInspectArguments]: true, [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, [FuseV1Options.OnlyLoadAppFromAsar]: true, }), diff --git a/package-lock.json b/package-lock.json index 3a8198926..c6a3c5ae1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@typescript-eslint/typescript-estree": "^8.21.0", "@vitejs/plugin-react": "^4.3.1", "allotment": "^1.20.2", + "astring": "^1.9.0", "chokidar": "^4.0.3", "clsx": "^2.1.1", "constrained-editor-plugin": "^1.3.0", @@ -91,6 +92,7 @@ "@electron-forge/plugin-vite": "^7.4.0", "@electron-forge/publisher-github": "^7.4.0", "@electron/fuses": "^1.8.0", + "@playwright/test": "^1.52.0", "@tanstack/eslint-plugin-query": "^5.53.0", "@testing-library/react": "^16.0.1", "@types/chrome": "^0.0.287", @@ -109,6 +111,7 @@ "@typescript-eslint/types": "^8.21.0", "dotenv": "^16.4.7", "electron": "30.0.8", + "electron-playwright-helpers": "^1.7.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", @@ -2971,6 +2974,21 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -9359,6 +9377,15 @@ "node": ">=4" } }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", @@ -11645,6 +11672,15 @@ "node": ">= 14" } }, + "node_modules/electron-playwright-helpers": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/electron-playwright-helpers/-/electron-playwright-helpers-1.7.1.tgz", + "integrity": "sha512-S9mo7LfpERgub2WIuYVPpib4XKFeAqBP+mxYf5Bv7E0B5GUB+LUbSj6Fpu39h18Ar635Nf9nQYTmypjuvaYJng==", + "dev": true, + "dependencies": { + "@electron/asar": "^3.2.4" + } + }, "node_modules/electron-squirrel-startup": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.1.tgz", @@ -17710,6 +17746,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 69a9e1f00..ec21a6834 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "publish": "electron-forge publish", "lint": "eslint --ext .ts,.tsx .", "test": "vitest run", + "flow-test": "playwright test -- tests", "test:watch": "vitest watch", "format": "prettier --ignore-unknown --write \"./**/*\"", "prepare": "husky && husky install" @@ -33,6 +34,7 @@ "@electron-forge/plugin-vite": "^7.4.0", "@electron-forge/publisher-github": "^7.4.0", "@electron/fuses": "^1.8.0", + "@playwright/test": "^1.52.0", "@tanstack/eslint-plugin-query": "^5.53.0", "@testing-library/react": "^16.0.1", "@types/chrome": "^0.0.287", @@ -51,6 +53,7 @@ "@typescript-eslint/types": "^8.21.0", "dotenv": "^16.4.7", "electron": "30.0.8", + "electron-playwright-helpers": "^1.7.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", @@ -106,6 +109,7 @@ "@typescript-eslint/typescript-estree": "^8.21.0", "@vitejs/plugin-react": "^4.3.1", "allotment": "^1.20.2", + "astring": "^1.9.0", "chokidar": "^4.0.3", "clsx": "^2.1.1", "constrained-editor-plugin": "^1.3.0", diff --git a/src/main/script.ts b/src/main/script.ts index ab1cc6d85..e7ed64be3 100644 --- a/src/main/script.ts +++ b/src/main/script.ts @@ -1,11 +1,9 @@ import { parse, TSESTree as ts } from '@typescript-eslint/typescript-estree' +import { generate } from 'astring' import { app, dialog, BrowserWindow } from 'electron' import { readFile, writeFile, unlink } from 'fs/promises' import { spawn, ChildProcessWithoutNullStreams } from 'node:child_process' import path from 'path' -import { format } from 'prettier' -// eslint-disable-next-line import/default -import estree from 'prettier/plugins/estree' import readline from 'readline/promises' import { @@ -320,25 +318,7 @@ export const enhanceScript = async ({ }) ) - return format(script, { - parser: 'k6', - plugins: [ - estree, - // This is a custom parser plugin that simply returns our modified AST. - { - parsers: { - k6: { - astFormat: 'estree', - parse: () => { - return scriptAst - }, - locStart: () => 0, - locEnd: () => 0, - }, - }, - }, - ], - }) + return generate(scriptAst) } const getSnippetPath = (snippetName: string) => { diff --git a/tests/launch.spec.ts b/tests/launch.spec.ts new file mode 100644 index 000000000..997218810 --- /dev/null +++ b/tests/launch.spec.ts @@ -0,0 +1,132 @@ +import { test, expect, _electron as electron } from '@playwright/test' +import type { ElectronApplication, Page } from '@playwright/test' +import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers' + +let electronApp: ElectronApplication +let mainWindow: Page +let splashscreenWindow: Page + +test.beforeAll(async () => { + const latestBuild = findLatestBuild() + const appInfo = parseElectronApp(latestBuild) + + electronApp = await electron.launch({ + args: [appInfo.main], + executablePath: appInfo.executable, + }) + + // splashscreen + splashscreenWindow = await electronApp.firstWindow() + mainWindow = await electronApp.waitForEvent('window') +}) + +test.afterAll(async () => { + // const pid = electronApp.process().pid + // if (pid !== undefined) { + // process.kill(pid) + // } + await electronApp.close() + // on macos-latest closing might get stuck so we force kill it if we finished testing + // try { + // await electronApp.close() + // } catch (error) { + // console.error('Normal close failed, forcing close:', error) + // const pid = electronApp.process().pid + // if (pid !== undefined) { + // process.kill(pid) + // } + // } +}) + +test('launch app', async () => { + const title = await mainWindow.title() + expect(title).toBe('Grafana k6 Studio') + + const body = splashscreenWindow.locator('body') + // not loading without this await for some reason + await body.count() + expect(body).toBeVisible() +}) + +test('start recording', async () => { + const testUrl = 'quickpizza.grafana.com' + const recordingButton = mainWindow.getByRole('link', { name: /record flow/i }) + await recordingButton.click() + + // insert test url + const urlInput = mainWindow.getByRole('textbox', { name: /e\.g\./i }) + await urlInput.fill(testUrl) + + // start recording button + const startRecording = mainWindow.getByRole('button', { + name: /start recording/i, + }) + expect(startRecording).toBeVisible() + + await startRecording.click() + + // requests are getting recorded, check row appears with quickpizza + const pizzaRows = mainWindow.locator('tr:has-text("quickpizza.grafana.com")') + await pizzaRows.first().waitFor() + + expect(pizzaRows.first()).toBeVisible() + + // stop recording + const stopRecordingButton = mainWindow.getByRole('button', { + name: /Stop recording/i, + }) + expect(stopRecordingButton).toBeVisible() + await stopRecordingButton.click() + + // assert we have the create test generator button + const createTestGeneratorButton = mainWindow.getByRole('button', { + name: /Create test generator/i, + }) + await createTestGeneratorButton.waitFor() + expect(createTestGeneratorButton).toBeVisible() +}) + +test('create generator', async () => { + // press the create test generator button + const createTestGeneratorButton = mainWindow.getByRole('button', { + name: /Create test generator/i, + }) + await createTestGeneratorButton.click() + + // on the allowlist popup, press continue + const allowlistContinue = mainWindow.getByRole('button', { name: 'Continue' }) + await allowlistContinue.click() + + // add new Custom code rule + const addRule = mainWindow.getByRole('button', { name: 'Add rule' }) + await addRule.first().click() + + const customCode = mainWindow.getByRole('menuitem', { name: 'Custom code' }) + await customCode.click() + + // type in the rule + const editor = mainWindow.getByRole('code').nth(1) + await editor.click() + await mainWindow.keyboard.type("console.log('hello test')") + + // save the generator + const save = mainWindow.getByRole('button', { name: 'Save generator' }) + await save.click() + + // validate script + const scriptTab = mainWindow.getByRole('tab', { name: 'Script' }) + await scriptTab.click() + + const validate = mainWindow.getByRole('button', { name: 'Validate' }) + await validate.click() + + // close button of Validator dialog is visible + const closeValidator = mainWindow.getByRole('button', { name: 'Close' }) + await closeValidator.waitFor() + expect(closeValidator).toBeVisible() + await closeValidator.click() + + // go back to Home + const home = mainWindow.getByRole('link', { name: 'Home' }) + await home.first().click() +}) diff --git a/vitest.config.ts b/vitest.config.ts index 015ebd48d..51e6f2574 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,11 +1,12 @@ import viteTsconfigPaths from 'vite-tsconfig-paths' -import { defineConfig } from 'vitest/config' +import { defineConfig, configDefaults } from 'vitest/config' export default defineConfig({ plugins: [viteTsconfigPaths()], test: { includeSource: ['src/**/*.{js,ts}'], environment: 'jsdom', + exclude: [...configDefaults.exclude, 'tests/*'], }, define: { 'import.meta.vitest': 'undefined',