From 6c6f66e3fa4ca9931974e7458e06bf73e27aeb14 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 13 Mar 2024 12:55:38 +0000 Subject: [PATCH 01/38] Initialise playwright Done by running `npm init playwright@latest`. Install playwright and added some example tests --- .gitignore | 4 + e2e/example.spec.ts | 18 + package-lock.json | 894 +++------------------------ package.json | 1 + playwright.config.ts | 77 +++ tests-examples/demo-todo-app.spec.ts | 437 +++++++++++++ 6 files changed, 609 insertions(+), 822 deletions(-) create mode 100644 e2e/example.spec.ts create mode 100644 playwright.config.ts create mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/.gitignore b/.gitignore index be4846622..232c2e487 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ __pycache__ *.pyc /reports/* /crowdin/* +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts new file mode 100644 index 000000000..54a906a4e --- /dev/null +++ b/e2e/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/package-lock.json b/package-lock.json index 4cae41190..f9fc36e03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", + "@playwright/test": "^1.42.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", @@ -1996,69 +1997,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", @@ -2075,294 +2013,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2834,6 +2484,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "dev": true, + "dependencies": { + "playwright": "1.42.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2857,168 +2522,24 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", "cpu": [ - "x64" + "arm64" ], "optional": true, "os": [ - "win32" + "darwin" ] }, "node_modules/@sanity/block-content-to-hyperscript": { @@ -8853,6 +8374,50 @@ "pathe": "^1.1.0" } }, + "node_modules/playwright": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "dev": true, + "dependencies": { + "playwright-core": "1.42.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "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/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -10569,51 +10134,6 @@ "vite": "^2.6.0 || 3 || 4 || 5" } }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", @@ -10629,276 +10149,6 @@ "node": ">=12" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/vite/node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", diff --git a/package.json b/package.json index dff92d79a..be038e6ba 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", + "@playwright/test": "^1.42.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..a2811c035 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 000000000..2fd6016fe --- /dev/null +++ b/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +]; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} From e4a67b098da60221f7b75a087834feaeb7d0075d Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 13 Mar 2024 13:12:51 +0000 Subject: [PATCH 02/38] Downgrade to @playwright/test v1.30.0 Otherwise there is a Node error --- package-lock.json | 55 +++++++++++------------------------------------ package.json | 2 +- 2 files changed, 13 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9fc36e03..d8009e190 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "^1.42.1", + "@playwright/test": "1.30.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", @@ -2485,18 +2485,19 @@ } }, "node_modules/@playwright/test": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", - "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", + "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", "dev": true, "dependencies": { - "playwright": "1.42.1" + "@types/node": "*", + "playwright-core": "1.30.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=14" } }, "node_modules/@popperjs/core": { @@ -8374,48 +8375,16 @@ "pathe": "^1.1.0" } }, - "node_modules/playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", - "dev": true, - "dependencies": { - "playwright-core": "1.42.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, "node_modules/playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", + "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", "dev": true, "bin": { - "playwright-core": "cli.js" + "playwright": "cli.js" }, "engines": { - "node": ">=16" - } - }, - "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": ">=14" } }, "node_modules/possible-typed-array-names": { diff --git a/package.json b/package.json index be038e6ba..4923dbe8b 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "^1.42.1", + "@playwright/test": "1.30.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", From 3b1e7f4421758f960e3e85d6618e79265c4bb08c Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 13 Mar 2024 13:19:37 +0000 Subject: [PATCH 03/38] Configure Playwright - remove comments - only run test in Chromium browser - use testDir to ./src/e2e - run local server before starting tests --- playwright.config.ts | 52 +++++--------------------------------------- 1 file changed, 6 insertions(+), 46 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index a2811c035..93557373f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,23 +10,13 @@ import { defineConfig, devices } from "@playwright/test"; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: "./e2e", - /* Run tests in files in parallel */ + testDir: "./", fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", }, @@ -36,42 +26,12 @@ export default defineConfig({ name: "chromium", use: { ...devices["Desktop Chrome"] }, }, - - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, - - { - name: "webkit", - use: { ...devices["Desktop Safari"] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + webServer: { + command: "npm run start", + url: "http://127.0.0.1:3000", + reuseExistingServer: !process.env.CI, + }, }); From dadd592045cff946a09291c733be7f4b598e4ab4 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 13 Mar 2024 15:02:40 +0000 Subject: [PATCH 04/38] Upgrade playwright/test to v1.32.0 so that we can use `npx playwright test --ui` for dev --- package-lock.json | 33 +++++++++++++++++++++++++-------- package.json | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8009e190..0a721b3a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "1.30.0", + "@playwright/test": "1.32.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", @@ -2485,19 +2485,36 @@ } }, "node_modules/@playwright/test": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", - "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz", + "integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==", "dev": true, "dependencies": { "@types/node": "*", - "playwright-core": "1.30.0" + "playwright-core": "1.32.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=14" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/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/@popperjs/core": { @@ -8376,9 +8393,9 @@ } }, "node_modules/playwright-core": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", - "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz", + "integrity": "sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==", "dev": true, "bin": { "playwright": "cli.js" diff --git a/package.json b/package.json index 4923dbe8b..88f35510e 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "1.30.0", + "@playwright/test": "1.32.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", From 0f212a5ccfaf9d13560d06d1c28b15d8e8534db1 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 13 Mar 2024 15:03:09 +0000 Subject: [PATCH 05/38] Add settings.spec.ts --- playwright.config.ts | 10 ++-- src/e2e/app-playwright.ts | 88 ++++++++++++++++++++++++++++++++++++ src/e2e/app-test-fixtures.ts | 12 +++++ src/e2e/settings.spec.ts | 27 +++++++++++ 4 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 src/e2e/app-playwright.ts create mode 100644 src/e2e/app-test-fixtures.ts create mode 100644 src/e2e/settings.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 93557373f..f8cc8a17c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,16 +1,10 @@ import { defineConfig, devices } from "@playwright/test"; -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: "./", + testDir: "./src/e2e", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, @@ -19,6 +13,8 @@ export default defineConfig({ use: { trace: "on-first-retry", }, + // ignore *.test.ts for now + testMatch: "*.spec.ts", /* Configure projects for major browsers */ projects: [ diff --git a/src/e2e/app-playwright.ts b/src/e2e/app-playwright.ts new file mode 100644 index 000000000..9a9c88815 --- /dev/null +++ b/src/e2e/app-playwright.ts @@ -0,0 +1,88 @@ +/** + * (c) 2021 - 2022, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { Page, expect } from "@playwright/test"; +import { Flag } from "../flags"; + +export enum LoadDialogType { + CONFIRM, + REPLACE, + CONFIRM_BUT_LOAD_AS_MODULE, + NONE, +} + +export interface BrowserDownload { + filename: string; + data: Buffer; +} + +const defaultWaitForOptions = { timeout: 10_000 }; + +const baseUrl = "http://localhost:3000"; +const reportsPath = "reports/e2e/"; + +interface UrlOptions { + /** + * Flags. + * + * "none" and "noWelcome" are always added. + * + * Do not use "*", instead explicitly enable the set of flags your test requires. + */ + flags?: Flag[]; + /** + * URL fragment including the #. + */ + fragment?: string; + /** + * Language parameter passed via URL. + */ + language?: string; +} + +export class App { + constructor(public readonly page: Page) {} + + async goto(options: UrlOptions = {}) { + this.page.goto(this.optionsToURL(options)); + } + + private optionsToURL(options: UrlOptions): string { + const flags = new Set([ + "none", + "noWelcome", + ...(options.flags ?? []), + ]); + const params: Array<[string, string]> = Array.from(flags).map((f) => [ + "flag", + f, + ]); + if (options.language) { + params.push(["l", options.language]); + } + return ( + baseUrl + + // We didn't use BASE_URL here as CRA seems to set it to "" before running jest. + // Maybe can be changed since the Vite upgrade. + (process.env.E2E_BASE_URL ?? "/") + + "?" + + new URLSearchParams(params) + + (options.fragment ?? "") + ); + } + + async expectProjectName(match: string) { + await expect( + this.page.getByTestId("project-name").getByText(match) + ).toBeVisible(); + } + + async switchLanguage(locale: string) { + // All test ids so they can be language invariant. + await this.page.getByTestId("settings").click(); + await this.page.getByTestId("language").click(); + await this.page.getByTestId(locale).click(); + } +} diff --git a/src/e2e/app-test-fixtures.ts b/src/e2e/app-test-fixtures.ts new file mode 100644 index 000000000..565efe98b --- /dev/null +++ b/src/e2e/app-test-fixtures.ts @@ -0,0 +1,12 @@ +import { test as base } from "@playwright/test"; +import { App } from "./app-playwright.js"; + +type MyFixtures = { + app: App; +}; + +export const test = base.extend({ + app: async ({ page }, use) => { + await use(new App(page)); + }, +}); diff --git a/src/e2e/settings.spec.ts b/src/e2e/settings.spec.ts new file mode 100644 index 000000000..26a70587f --- /dev/null +++ b/src/e2e/settings.spec.ts @@ -0,0 +1,27 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +test.describe("settings", () => { + test("sets language via URL", async ({ app }) => { + await app.goto({ language: "fr" }); + // French via the URL + await app.expectProjectName("Projet sans titre"); + + await app.switchLanguage("en"); + await app.page.reload(); + // French URL ignored as we've made an explicit language choice. + await app.expectProjectName("Untitled project"); + }); + + test("switches language", async ({ app }) => { + await app.goto(); + await app.switchLanguage("fr"); + await app.expectProjectName("Projet sans titre"); + await app.switchLanguage("en"); + await app.expectProjectName("Untitled project"); + }); +}); From be55bc446a79c84093d34271016ba576ef243453 Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 13 Mar 2024 16:27:36 +0000 Subject: [PATCH 06/38] Add reset.spec.ts --- src/e2e/app-playwright.ts | 50 +++++++++++++++++++++++++++++++++++++-- src/e2e/reset.spec.ts | 23 ++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/e2e/reset.spec.ts diff --git a/src/e2e/app-playwright.ts b/src/e2e/app-playwright.ts index 9a9c88815..ffda76076 100644 --- a/src/e2e/app-playwright.ts +++ b/src/e2e/app-playwright.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: MIT */ -import { Page, expect } from "@playwright/test"; +import { Locator, Page, expect } from "@playwright/test"; import { Flag } from "../flags"; export enum LoadDialogType { @@ -43,7 +43,11 @@ interface UrlOptions { } export class App { - constructor(public readonly page: Page) {} + private codeTextArea: Locator; + + constructor(public readonly page: Page) { + this.codeTextArea = this.page.getByTestId("editor").getByRole("textbox"); + } async goto(options: UrlOptions = {}) { this.page.goto(this.optionsToURL(options)); @@ -85,4 +89,46 @@ export class App { await this.page.getByTestId("language").click(); await this.page.getByTestId(locale).click(); } + + async setProjectName(projectName: string): Promise { + await this.page.getByRole("button", { name: "Edit project name" }).click(); + await this.page.getByLabel("Name*").fill(projectName); + await this.page.getByRole("button", { name: "Confirm" }).click(); + } + + async selectAllInEditor(): Promise { + await this.codeTextArea.click(); + const metaOrCtrKey = process.platform === "darwin" ? "Meta" : "Control"; + await this.page.keyboard.press(`${metaOrCtrKey}+A`); + } + + async typeInEditor(text: string): Promise { + this.codeTextArea.fill(text); + } + + async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { + await this.page.getByRole("tab", { name: tabName }).click(); + } + + async createNewFile(name: string): Promise { + await this.switchTab("Project"); + await this.page.getByRole("button", { name: "Create file" }).click(); + await this.page.getByLabel("Name*").fill(name); + await this.page.getByRole("button", { name: "Create" }).click(); + } + + async resetProject(): Promise { + await this.switchTab("Project"); + await this.page.getByRole("button", { name: "Reset project" }).click(); + await this.page.getByRole("button", { name: "Replace" }).click(); + } + + async expectVisibleEditorContents(match: RegExp | string) { + return expect(this.codeTextArea).toContainText(match); + } + + async expectProjectFiles(expected: string[]): Promise { + await this.switchTab("Project"); + await expect(this.page.getByRole("listitem")).toHaveText(expected); + } } diff --git a/src/e2e/reset.spec.ts b/src/e2e/reset.spec.ts new file mode 100644 index 000000000..6865a5aba --- /dev/null +++ b/src/e2e/reset.spec.ts @@ -0,0 +1,23 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +test.describe("reset", () => { + test("sets language via URL", async ({ app }) => { + await app.goto(); + await app.setProjectName("My project"); + await app.selectAllInEditor(); + await app.typeInEditor("# Not the default starter code"); + await app.createNewFile("testing"); + + await app.resetProject(); + + // Everything's back to normal. + await app.expectProjectName("Untitled project"); + await app.expectVisibleEditorContents("from microbit import"); + await app.expectProjectFiles(["main.py"]); + }); +}); From 09f20ab150f4228bbd284f249df36aaaeec4b7b3 Mon Sep 17 00:00:00 2001 From: Grace Date: Thu, 14 Mar 2024 13:41:38 +0000 Subject: [PATCH 07/38] Add more test files WIP --- package-lock.json | 42 +++ package.json | 1 + playwright.config.ts | 7 +- src/e2e/app-playwright.ts | 452 +++++++++++++++++++++++++++++---- src/e2e/documentation.spec.ts | 95 +++++++ src/e2e/edits.spec.ts | 40 +++ src/e2e/migration.spec.ts | 32 +++ src/e2e/multiple-files.spec.ts | 90 +++++++ src/e2e/open.spec.ts | 149 +++++++++++ src/e2e/reset.spec.ts | 6 +- src/e2e/save.spec.ts | 73 ++++++ src/e2e/settings.spec.ts | 8 +- 12 files changed, 935 insertions(+), 60 deletions(-) create mode 100644 src/e2e/documentation.spec.ts create mode 100644 src/e2e/edits.spec.ts create mode 100644 src/e2e/migration.spec.ts create mode 100644 src/e2e/multiple-files.spec.ts create mode 100644 src/e2e/open.spec.ts create mode 100644 src/e2e/save.spec.ts diff --git a/package-lock.json b/package-lock.json index 0a721b3a5..d8694c232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", + "playwright": "^1.42.1", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", @@ -8392,6 +8393,23 @@ "pathe": "^1.1.0" } }, + "node_modules/playwright": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "dependencies": { + "playwright-core": "1.42.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/playwright-core": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz", @@ -8404,6 +8422,30 @@ "node": ">=14" } }, + "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==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/package.json b/package.json index 88f35510e..d6f8eff56 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", + "playwright": "^1.42.1", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", diff --git a/playwright.config.ts b/playwright.config.ts index f8cc8a17c..6da0e0d60 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -20,7 +20,12 @@ export default defineConfig({ projects: [ { name: "chromium", - use: { ...devices["Desktop Chrome"] }, + use: { + ...devices["Desktop Chrome"], + launchOptions: { + channel: "chromium-tip-of-tree", + }, + }, }, ], diff --git a/src/e2e/app-playwright.ts b/src/e2e/app-playwright.ts index ffda76076..4ee01e743 100644 --- a/src/e2e/app-playwright.ts +++ b/src/e2e/app-playwright.ts @@ -5,6 +5,9 @@ */ import { Locator, Page, expect } from "@playwright/test"; import { Flag } from "../flags"; +import path from "path"; +import { fileURLToPath } from "url"; +import { readFileSync } from "fs"; export enum LoadDialogType { CONFIRM, @@ -18,66 +21,127 @@ export interface BrowserDownload { data: Buffer; } -const defaultWaitForOptions = { timeout: 10_000 }; - const baseUrl = "http://localhost:3000"; -const reportsPath = "reports/e2e/"; interface UrlOptions { - /** - * Flags. - * - * "none" and "noWelcome" are always added. - * - * Do not use "*", instead explicitly enable the set of flags your test requires. - */ flags?: Flag[]; - /** - * URL fragment including the #. - */ fragment?: string; - /** - * Language parameter passed via URL. - */ language?: string; } +interface SaveOptions { + waitForDownload: boolean; +} + +class LoadDialog { + private confirmButton: Locator; + private replaceButton: Locator; + private optionsButton: Locator; + private type: LoadDialogType; + + constructor(public readonly page: Page, type: LoadDialogType) { + this.type = type; + this.confirmButton = this.page.getByRole("button", { name: "Confirm" }); + this.replaceButton = this.page.getByRole("button", { name: "Replace" }); + this.optionsButton = this.page.getByRole("button", { + name: "Options", + exact: true, + }); + } + + async submit() { + switch (this.type) { + case LoadDialogType.CONFIRM: + return await this.confirmButton.click(); + case LoadDialogType.REPLACE: + return await this.replaceButton.click(); + case LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE: + await this.optionsButton.click(); + await this.page.getByText(/^(Add|Replace) file .+\.py$/).click(); + return await this.confirmButton.click(); + default: + return; + } + } +} + +class FileActionsMenu { + public saveButton: Locator; + public editButton: Locator; + public deleteButton: Locator; + + constructor(public readonly page: Page, filename: string) { + this.saveButton = this.page.getByRole("menuitem", { + name: `Save ${filename}`, + }); + this.editButton = this.page.getByRole("menuitem", { + name: `Edit ${filename}`, + }); + this.deleteButton = this.page.getByRole("menuitem", { + name: `Delete ${filename}`, + }); + } + + async delete() { + await this.deleteButton.waitFor(); + await this.deleteButton.click(); + await this.page.getByRole("button", { name: "Delete" }).click(); + } +} + +class ProjectTabPanel { + private openButton: Locator; + constructor(public readonly page: Page) { + this.openButton = this.page + .getByRole("tabpanel", { name: "Project" }) + .getByTestId("open"); + } + + async openFileActionsMenu(filename: string) { + const fileActionsMenu = this.page.getByRole("button", { + name: `${filename} file actions`, + }); + await fileActionsMenu.waitFor(); + await fileActionsMenu.click(); + return new FileActionsMenu(this.page, filename); + } + + async chooseFile(filePathFromProjectRoot: string) { + const filePath = getAbsoluteFilePath(filePathFromProjectRoot); + const fileChooserPromise = this.page.waitForEvent("filechooser"); + await this.openButton.click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(filePath); + } +} + export class App { - private codeTextArea: Locator; + public editorTextArea: Locator; + private settingsButton: Locator; + public saveButton: Locator; + private searchButton: Locator; + public modifierKey: string; + public projectTab: ProjectTabPanel; constructor(public readonly page: Page) { - this.codeTextArea = this.page.getByTestId("editor").getByRole("textbox"); + this.editorTextArea = this.page.getByTestId("editor").getByRole("textbox"); + this.projectTab = new ProjectTabPanel(page); + this.settingsButton = this.page.getByTestId("settings"); + this.saveButton = this.page.getByRole("button", { + name: "Save", + exact: true, + }); + this.searchButton = this.page.getByRole("button", { name: "Search" }); + const isMac = process.platform === "darwin"; + this.modifierKey = isMac ? "Meta" : "Control"; } async goto(options: UrlOptions = {}) { - this.page.goto(this.optionsToURL(options)); - } - - private optionsToURL(options: UrlOptions): string { - const flags = new Set([ - "none", - "noWelcome", - ...(options.flags ?? []), - ]); - const params: Array<[string, string]> = Array.from(flags).map((f) => [ - "flag", - f, - ]); - if (options.language) { - params.push(["l", options.language]); - } - return ( - baseUrl + - // We didn't use BASE_URL here as CRA seems to set it to "" before running jest. - // Maybe can be changed since the Vite upgrade. - (process.env.E2E_BASE_URL ?? "/") + - "?" + - new URLSearchParams(params) + - (options.fragment ?? "") - ); + await this.page.goto(optionsToURL(options)); } - async expectProjectName(match: string) { + // TODO: Rename to expectProjectName + async findProjectName(match: string) { await expect( this.page.getByTestId("project-name").getByText(match) ).toBeVisible(); @@ -85,7 +149,7 @@ export class App { async switchLanguage(locale: string) { // All test ids so they can be language invariant. - await this.page.getByTestId("settings").click(); + await this.settingsButton.click(); await this.page.getByTestId("language").click(); await this.page.getByTestId(locale).click(); } @@ -97,13 +161,31 @@ export class App { } async selectAllInEditor(): Promise { - await this.codeTextArea.click(); - const metaOrCtrKey = process.platform === "darwin" ? "Meta" : "Control"; - await this.page.keyboard.press(`${metaOrCtrKey}+A`); + await this.editorTextArea.click(); + await this.page.keyboard.press(`${this.modifierKey}+A`); + } + + // TODO: Rename to pasteInEditor + async pasteToolkitCode() { + // Simulating keyboard press CTRL+V works in Playwright, + // but does not work in this case potentially due to + // CodeMirror pasting magic + const clipboardText: string = await this.page.evaluate( + "navigator.clipboard.readText()" + ); + await this.editorTextArea.evaluate((el, clipboardText1) => { + const text = clipboardText1; + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + const clipboardEvent = new ClipboardEvent("paste", { + clipboardData, + }); + el.dispatchEvent(clipboardEvent); + }, clipboardText); } async typeInEditor(text: string): Promise { - this.codeTextArea.fill(text); + await this.editorTextArea.fill(text); } async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { @@ -123,12 +205,278 @@ export class App { await this.page.getByRole("button", { name: "Replace" }).click(); } - async expectVisibleEditorContents(match: RegExp | string) { - return expect(this.codeTextArea).toContainText(match); + // TODO: Rename to expectEditorContentsContain + // Use allInnerTexts() for matching text + async findVisibleEditorContents(match: RegExp | string) { + // Scroll to the top of code text area + await this.editorTextArea.click(); + await this.page.mouse.wheel(0, -100000000); + return expect(this.editorTextArea).toContainText(match); } - async expectProjectFiles(expected: string[]): Promise { + // TODO: Rename to expectProjectFiles + async findProjectFiles(expected: string[]): Promise { await this.switchTab("Project"); await expect(this.page.getByRole("listitem")).toHaveText(expected); } + + async loadFiles( + filePathFromProjectRoot: string, + options: { acceptDialog?: LoadDialogType } = {} + ) { + await this.switchTab("Project"); + await this.projectTab.chooseFile(filePathFromProjectRoot); + + if (options.acceptDialog !== undefined) { + const loadDialog = new LoadDialog(this.page, options.acceptDialog); + await loadDialog.submit(); + } + } + + async dropFile( + filePathFromProjectRoot: string, + options: { acceptDialog?: LoadDialogType } = {} + ) { + const filePath = getAbsoluteFilePath(filePathFromProjectRoot); + const filename = getFilename(filePathFromProjectRoot); + + // wait for page to load + await this.saveButton.waitFor(); + + // Playwright drag and drop file method taken from + // https://github.com/microsoft/playwright/issues/10667#issuecomment-998397241 + const buffer = readFileSync(filePath, { encoding: "ascii" }); + const dataTransfer = await this.page.evaluateHandle( + ({ buffer, filename }) => { + const dt = new DataTransfer(); + const file = new File([buffer], filename); + dt.items.add(file); + return dt; + }, + { buffer, filename } + ); + + // Drag file over target area to reveal drop zone + await this.page + .getByTestId("project-drop-target") + .dispatchEvent("dragover", { dataTransfer }); + + const dropZone = this.page.getByTestId("project-drop-target-overlay"); + await dropZone.waitFor(); + await dropZone.dispatchEvent("drop", { dataTransfer }); + + if (options.acceptDialog !== undefined) { + const loadDialog = new LoadDialog(this.page, options.acceptDialog); + await loadDialog.submit(); + } + } + + // TODO: Rename to expectAlertText + async findAlertText(title: string, description?: string): Promise { + await expect(this.page.getByText(title)).toBeVisible(); + if (description) { + await expect(this.page.getByText(description)).toBeVisible(); + } + } + + async isDeleteFileOptionDisabled(filename: string) { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + return await fileOptionMenu.deleteButton.isDisabled(); + } + + async isEditFileOptionDisabled(filename: string) { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + return await fileOptionMenu.editButton.isDisabled(); + } + + // TODO: Rename to editFile + async switchToEditing(filename: string): Promise { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + await fileOptionMenu.editButton.click(); + } + + async findThirdPartyModuleWarning( + expectedName: string, + expectedVersion: string + ): Promise { + for (const name in [expectedName, expectedVersion]) { + await expect(this.page.getByRole("cell", { name })).toBeVisible(); + } + } + + async closeDialog(dialogText: string) { + await this.page.getByText(dialogText).waitFor(); + await this.page.getByRole("button", { name: "Close" }).first().click(); + } + + async save(options: SaveOptions = { waitForDownload: true }) { + if (!options.waitForDownload) { + await this.saveButton.click(); + return; + } + const downloadPromise = this.page.waitForEvent("download"); + await this.saveButton.click(); + return await downloadPromise; + } + + // TODO: Rename to savePythonScript + async saveMain() { + await this.page.getByTestId("more-save-options").click(); + const downloadPromise = this.page.waitForEvent("download"); + await this.page + .getByRole("menuitem", { name: "Save Python script" }) + .click(); + await downloadPromise; + } + + // TODO: Rename to expectDialog + async confirmInputDialog(text: string) { + await expect(this.page.getByText(text)).toBeVisible(); + } + + async deleteFile(filename: string) { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + await fileOptionMenu.delete(); + } + + async toggleSettingThirdPartyModuleEditing(): Promise { + await this.settingsButton.click(); + await this.page.getByRole("menuitem", { name: "Settings" }).click(); + await this.page + .getByText("Allow editing third-party modules", { exact: true }) + .click(); + await this.page.getByRole("button", { name: "Close" }).click(); + } + + // Rename to closeAndExpectBeforeUnloadDialogVisible + async closePageCheckDialog(visible: boolean): Promise { + this.page.on("dialog", async (dialog) => { + expect(dialog.type() === "beforeunload").toEqual(visible); + await dialog.dismiss(); + }); + await this.page.close({ runBeforeUnload: true }); + } + + // Rename to expectDocumentationTopLevelHeading + async findDocumentationTopLevelHeading( + title: string, + description?: string + ): Promise { + await expect( + this.page.getByRole("heading", { name: title, exact: true }) + ).toBeVisible(); + if (description) { + await expect(this.page.getByText(description)).toBeVisible(); + } + } + + async selectDocumentationSection(name: string): Promise { + await this.page.getByRole("heading", { name }).click(); + } + + // Rename srollToTop + async triggerScroll(_tabName: string) { + await this.page.mouse.wheel(0, -100000000); + } + + async toggleCodeActionButton(name: string): Promise { + await this.page + .getByRole("listitem") + .filter({ hasText: name }) + .getByRole("button", { name: "More" }) + .click(); + } + + async selectToolkitDropDownOption( + label: string, + option: string + ): Promise { + await this.page.getByRole("combobox", { name: label }).selectOption(option); + } + + private getCodeExample(name: string) { + return this.page + .getByRole("listitem") + .filter({ hasText: name }) + .locator("div") + .filter({ + hasText: "Code example:", + }) + .nth(2); + } + + async copyCode(name: string) { + await this.getCodeExample(name).click(); + await this.page.getByRole("button", { name: "Copy code" }).click(); + } + + async dragDropCodeEmbed(name: string, targetLine: number) { + const codeExample = this.getCodeExample(name); + const editorLine = this.page + .getByTestId("editor") + .getByRole("textbox") + .locator("div") + .filter({ hasText: targetLine.toString() }); + + await codeExample.dragTo(editorLine); + } + + // TODO: Rename to search + async searchToolkits(searchText: string): Promise { + await this.switchTab("Reference"); + await this.searchButton.click(); + await this.page.getByPlaceholder("Search").fill(searchText); + } + + async selectFirstSearchResult(): Promise { + // wait for results to show + await this.page.getByRole("link").first().waitFor(); + const links = await this.page.getByRole("link").all(); + await links[0].click(); + } + + async selectDocumentationIdea(name: string): Promise { + await this.page.getByRole("button", { name }).click(); + } } + +export const getFilename = (filePath: string) => { + const filename = filePath.split("/").pop(); + if (!filename) { + throw new Error("dropFile Error: No filename found!"); + } + return filename; +}; + +const getAbsoluteFilePath = (filePathFromProjectRoot: string) => { + const dir = path.dirname(fileURLToPath(import.meta.url)); + return path.join(dir.replace("src/e2e", ""), filePathFromProjectRoot); +}; + +const optionsToURL = (options: UrlOptions): string => { + const flags = new Set([ + "none", + "noWelcome", + ...(options.flags ?? []), + ]); + const params: Array<[string, string]> = Array.from(flags).map((f) => [ + "flag", + f, + ]); + if (options.language) { + params.push(["l", options.language]); + } + return ( + baseUrl + + // We didn't use BASE_URL here as CRA seems to set it to "" before running jest. + // Maybe can be changed since the Vite upgrade. + (process.env.E2E_BASE_URL ?? "/") + + "?" + + new URLSearchParams(params) + + (options.fragment ?? "") + ); +}; diff --git a/src/e2e/documentation.spec.ts b/src/e2e/documentation.spec.ts new file mode 100644 index 000000000..9e10042b2 --- /dev/null +++ b/src/e2e/documentation.spec.ts @@ -0,0 +1,95 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +test.describe("documentation", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("API toolkit navigation", async ({ app }) => { + await app.switchTab("API"); + await app.findDocumentationTopLevelHeading( + "API", + "For usage and examples, see" + ); + }); + + test("Copy code and paste in editor", async ({ app, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const tab = "Reference"; + await app.selectAllInEditor(); + await app.typeInEditor("# Initial document"); + await app.switchTab(tab); + await app.selectDocumentationSection("Display"); + await app.triggerScroll(tab); + await app.toggleCodeActionButton("Images: built-in"); + await app.copyCode("Images: built-in"); + await app.pasteToolkitCode(); + await app.findVisibleEditorContents("display.show(Image.HEART)"); + }); + + test("Copy code after dropdown choice and paste in editor", async ({ + app, + context, + }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const tab = "Reference"; + await app.selectAllInEditor(); + await app.typeInEditor("# Initial document"); + await app.switchTab(tab); + await app.selectDocumentationSection("Display"); + await app.triggerScroll(tab); + await app.selectToolkitDropDownOption( + "Select image:", + "silly" // "Image.SILLY" + ); + await app.toggleCodeActionButton("Images: built-in"); + await app.copyCode("Images: built-in"); + + await app.pasteToolkitCode(); + await app.findVisibleEditorContents("display.show(Image.SILLY)"); + }); + + test("Insert code via drag and drop", async ({ app }) => { + await app.selectAllInEditor(); + await app.typeInEditor("#1\n#2\n#3\n"); + await app.findVisibleEditorContents("#2"); + await app.switchTab("Reference"); + await app.selectDocumentationSection("Display"); + await app.dragDropCodeEmbed("Scroll", 2); + + // There's some weird trailing whitespace in this snippet that needs fixing in the content. + const expected = + "from microbit import *display.scroll('score') display.scroll(23)#1#2#3"; + + await app.findVisibleEditorContents(expected); + }); + + test("Searches and navigates to the first result", async ({ app }) => { + await app.searchToolkits("loop"); + await app.selectFirstSearchResult(); + await app.findDocumentationTopLevelHeading( + "Loops", + "Count and repeat sets of instructions" + ); + }); + + test("Ideas tab navigation", async ({ app }) => { + await app.switchTab("Ideas"); + await app.findDocumentationTopLevelHeading( + "Ideas", + "Try out these projects, modify them and get inspired" + ); + }); + + test("Select an idea", async ({ app }) => { + const ideaName = "Emotion badge"; + await app.switchTab("Ideas"); + await app.selectDocumentationIdea(ideaName); + await app.findDocumentationTopLevelHeading(ideaName); + }); +}); diff --git a/src/e2e/edits.spec.ts b/src/e2e/edits.spec.ts new file mode 100644 index 000000000..efa202a83 --- /dev/null +++ b/src/e2e/edits.spec.ts @@ -0,0 +1,40 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +test.describe("edits", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("doesn't prompt on close if no edits made", async ({ app }) => { + await app.closePageCheckDialog(false); + }); + + test("prompts on close if file edited", async ({ app }) => { + await app.typeInEditor("A change!"); + await app.findVisibleEditorContents(/A change/); + + await app.closePageCheckDialog(true); + }); + + test("prompts on close if project name edited", async ({ app }) => { + const name = "idiosyncratic ruminant"; + await app.setProjectName(name); + await app.findProjectName(name); + + await app.closePageCheckDialog(true); + }); + + test("retains text across a reload via session storage", async ({ app }) => { + await app.typeInEditor("A change!"); + await app.findVisibleEditorContents(/A change/); + + await app.page.reload(); + + await app.findVisibleEditorContents(/A change/); + }); +}); diff --git a/src/e2e/migration.spec.ts b/src/e2e/migration.spec.ts new file mode 100644 index 000000000..e0acccb20 --- /dev/null +++ b/src/e2e/migration.spec.ts @@ -0,0 +1,32 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +const heartMigrationFragment = + "#project:XQAAgACRAAAAAAAAAAA9iImmlGSt1R++5LD+ZJ36cRz46B+lhYtNRoWF0nijpaVyZlK7ACfSpeoQpgfk21st4ty06R4PEOM4sSAXBT95G3en+tghrYmE+YJp6EiYgzA9ThKkyShWq2UdvmCzqxoNfYc1wlmTqlNv/Piaz3WoSe3flvr/ItyLl0aolQlEpv4LA8A="; + +const sunlightSensorMigrationFragment = + "#project:XQAAgAByAQAAAAAAAAA9iImmlGSt1R++5LD+ZJ36cRz46B+lhYtNRoWF0nijpaVyZlK7ACfSpeoQpgfk21st4ty06R4PEOW6kOsIEMK7SL0Qco7jgsHFKZXfjv/XcHWvXG9qyz1a/a3NUulFDj/FDJxVAIV+WZLpRoo4E6MbW70FOgIfBPWP2hDVsojpoLc7ZfKI8SHxv54FSfB5bkbzaAKO+8CO73t6Odtv691JGjJ9MExFighY6GxyM/DoNInDDpAjFeaqCWrYdwENX7ZVM3we8f4swI71tL28N7sg588aB//A78AA"; + +test.describe("migration", () => { + test("Loads the project from the URL", async ({ app }) => { + await app.goto({ fragment: heartMigrationFragment }); + await app.findProjectName("Hearts"); + await app.findVisibleEditorContents( + "from microbit import *display.show(Image.HEART)" + ); + + // Regression test: Check that we can switch to a different migration in the same session. + // Previously we ignored the migration because we already had content in session storage. + await app.goto({ + fragment: sunlightSensorMigrationFragment, + }); + await app.page.reload(); + // wait for page to load + await app.saveButton.waitFor(); + await app.findVisibleEditorContents("display.read_light_level"); + }); +}); diff --git a/src/e2e/multiple-files.spec.ts b/src/e2e/multiple-files.spec.ts new file mode 100644 index 000000000..884a11c43 --- /dev/null +++ b/src/e2e/multiple-files.spec.ts @@ -0,0 +1,90 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { expect } from "@playwright/test"; +import { LoadDialogType } from "./app-playwright.js"; +import { test } from "./app-test-fixtures.js"; + +test.describe("multiple-files", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("Copes with hex with no Python files", async ({ app }) => { + // Probably best for this to be an error or else we + // need to cope with no Python at all to display. + await app.loadFiles("src/micropython/main/microbit-micropython-v2.hex"); + await app.findAlertText( + "Cannot load file", + "No appended code found in the hex file" + ); + }); + + test("Add a new file", async ({ app }) => { + await app.createNewFile("test"); + + await app.findVisibleEditorContents(/Your new file/); + await app.findProjectFiles(["main.py", "test.py"]); + }); + + test("Prevents deleting main.py", async ({ app }) => { + expect(await app.isDeleteFileOptionDisabled("main.py")).toEqual(true); + }); + + test("Copes with non-main file being updated", async ({ app }) => { + await app.loadFiles("testData/usermodule.py", { + acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, + }); + await app.switchToEditing("usermodule.py"); + await app.findVisibleEditorContents(/b_works/); + + await app.loadFiles("testData/updated/usermodule.py", { + acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, + }); + + await app.findVisibleEditorContents(/c_works/); + }); + + test("Shows warning for third-party module", async ({ app }) => { + await app.loadFiles("testData/module.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + await app.switchToEditing("module.py"); + await app.findThirdPartyModuleWarning("a", "1.0.0"); + + await app.toggleSettingThirdPartyModuleEditing(); + try { + await app.findVisibleEditorContents(/a_works/); + } finally { + await app.toggleSettingThirdPartyModuleEditing(); + } + + await app.loadFiles("testData/updated/module.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + await app.findThirdPartyModuleWarning("a", "1.1.0"); + }); + + test("Copes with currently open file being deleted", async ({ app }) => { + await app.loadFiles("testData/module.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + await app.switchToEditing("module.py"); + + await app.deleteFile("module.py"); + + await app.findVisibleEditorContents(/Hello/); + }); + + test("Muddles through if given non-UTF-8 main.py", async ({ app }) => { + // We could start detect this on open but not sure it's worth it introducting the error cases. + // If we need to recreate the hex then just fill the file with 0xff. + await app.loadFiles("testData/invalid-utf-8.hex"); + + await app.findVisibleEditorContents( + /^����������������������������������������������������������������������������������������������������$/ + ); + }); +}); diff --git a/src/e2e/open.spec.ts b/src/e2e/open.spec.ts new file mode 100644 index 000000000..0c206b3ce --- /dev/null +++ b/src/e2e/open.spec.ts @@ -0,0 +1,149 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { expect } from "@playwright/test"; +import { LoadDialogType } from "./app-playwright.js"; +import { test } from "./app-test-fixtures.js"; + +test.describe("open", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("Shows an alert when loading a MakeCode hex", async ({ app }) => { + await app.loadFiles("testData/makecode.hex"); + + await app.findAlertText( + "Cannot load file", + "This hex file cannot be loaded in the Python Editor. The Python Editor cannot open hex files created with Microsoft MakeCode." + ); + }); + + test("Loads a Python file", async ({ app }) => { + await app.loadFiles("testData/samplefile.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + + await app.findAlertText("Updated file main.py"); + await app.findProjectName("Untitled project"); + }); + + test("Correctly handles a hex that's actually Python", async ({ app }) => { + await app.loadFiles("testData/not-a-hex.hex", { + acceptDialog: LoadDialogType.NONE, + }); + + await app.findAlertText( + "Cannot load file", + // Would be great to have custom messages here but needs error codes + // pushing into microbit-fs. + "Malformed .hex file, could not parse any registers" + ); + }); + + test("Loads a v1.0.1 hex file", async ({ app }) => { + await app.loadFiles("testData/1.0.1.hex"); + + await app.findVisibleEditorContents(/PASS1/); + await app.findProjectName("1.0.1"); + }); + + test("Loads a v0.9 hex file", async ({ app }) => { + await app.loadFiles("testData/0.9.hex"); + + await app.findVisibleEditorContents(/PASS2/); + await app.findProjectName("0.9"); + }); + + test("Loads via drag and drop", async ({ app }) => { + await app.dropFile("testData/1.0.1.hex"); + + await app.findProjectName("1.0.1"); + // await app.findVisibleEditorContents(/PASS1/); + }); + + test("Correctly handles an mpy file", async ({ app }) => { + await app.loadFiles("testData/samplempyfile.mpy", { + acceptDialog: LoadDialogType.NONE, + }); + + await app.findAlertText( + "Cannot load file", + "This version of the Python Editor doesn't currently support adding .mpy files." + ); + }); + + test("Correctly handles a file with an invalid extension", async ({ + app, + }) => { + await app.loadFiles("testData/sampletxtfile.txt", { + acceptDialog: LoadDialogType.CONFIRM, + }); + + expect(await app.isEditFileOptionDisabled("sampletxtfile.txt")).toEqual( + true + ); + }); + + test("Correctly imports modules with the 'magic comment' in the filesystem.", async ({ + app, + }) => { + await app.loadFiles("testData/module.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + + await app.findAlertText("Added file module.py"); + + await app.loadFiles("testData/module.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + await app.findAlertText("Updated file module.py"); + }); + + test("Warns before load if you have changes", async ({ app }) => { + await app.typeInEditor("# Different text"); + await app.loadFiles("testData/1.0.1.hex", { + acceptDialog: LoadDialogType.REPLACE, + }); + await app.findVisibleEditorContents(/PASS1/); + await app.findProjectName("1.0.1"); + }); + + test("No warn before load if you save hex", async ({ app }) => { + await app.setProjectName("Avoid dialog"); + await app.typeInEditor("# Different text"); + await app.save(); + await app.closeDialog("Project saved"); + + // No dialog accepted + await app.loadFiles("testData/1.0.1.hex"); + await app.findVisibleEditorContents(/PASS1/); + }); + + test("No warn before load if you save main file", async ({ app }) => { + await app.setProjectName("Avoid dialog"); + await app.typeInEditor("# Different text"); + await app.saveMain(); + + // No dialog accepted + await app.loadFiles("testData/1.0.1.hex"); + await app.findVisibleEditorContents(/PASS1/); + }); + + test("Warn before load if you save main file only and you have others", async ({ + app, + }) => { + await app.setProjectName("Avoid dialog"); + await app.typeInEditor("# Different text"); + await app.createNewFile("another"); + await app.saveMain(); + await app.closeDialog("Warning: Only main.py downloaded"); + + await app.loadFiles("testData/1.0.1.hex", { + acceptDialog: LoadDialogType.REPLACE, + }); + await app.findVisibleEditorContents(/PASS1/); + }); +}); diff --git a/src/e2e/reset.spec.ts b/src/e2e/reset.spec.ts index 6865a5aba..5158a6090 100644 --- a/src/e2e/reset.spec.ts +++ b/src/e2e/reset.spec.ts @@ -16,8 +16,8 @@ test.describe("reset", () => { await app.resetProject(); // Everything's back to normal. - await app.expectProjectName("Untitled project"); - await app.expectVisibleEditorContents("from microbit import"); - await app.expectProjectFiles(["main.py"]); + await app.findProjectName("Untitled project"); + await app.findVisibleEditorContents("from microbit import"); + await app.findProjectFiles(["main.py"]); }); }); diff --git a/src/e2e/save.spec.ts b/src/e2e/save.spec.ts new file mode 100644 index 000000000..2112beae1 --- /dev/null +++ b/src/e2e/save.spec.ts @@ -0,0 +1,73 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { expect } from "@playwright/test"; +import fs from "fs"; +import { LoadDialogType } from "./app-playwright.js"; +import { test } from "./app-test-fixtures.js"; + +test.describe("save", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("Download - save the default HEX asd", async ({ app }) => { + await app.setProjectName("idiosyncratic ruminant"); + const download = await app.save(); + if (!download) { + throw new Error("Invalid download"); + } + const filename = download.suggestedFilename(); + expect(filename).toEqual("idiosyncratic ruminant.hex"); + + const path = await download.path(); + if (!path) { + throw new Error("Invalid path"); + } + const contents = await fs.promises.readFile(path, { encoding: "ascii" }); + expect(contents).toMatch(/^:020000040000FA/); + }); + + test("Shows an error when trying to save a hex file if the Python code is too large", async ({ + app, + }) => { + // Set the project name to avoid calling the edit project name input dialog. + await app.setProjectName("not default name"); + await app.loadFiles("testData/too-large.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + await app.findVisibleEditorContents(/# Filler/); + await app.save({ waitForDownload: false }); + + await app.findAlertText( + "Failed to build the hex file", + "There is no storage space left." + ); + }); + + test("Shows the name your project dialog if the project name is the default", async ({ + app, + }) => { + await app.save({ waitForDownload: false }); + await app.confirmInputDialog("Name your project"); + }); + + test("Shows the post-save dialog after hex save", async ({ app }) => { + await app.setProjectName("not default name"); + await app.save(); + await app.confirmInputDialog("Project saved"); + }); + + test("Shows the multiple files dialog after main.py save if there are multiple files in the project", async ({ + app, + }) => { + await app.setProjectName("not default name"); + await app.loadFiles("testData/module.py", { + acceptDialog: LoadDialogType.CONFIRM, + }); + await app.saveMain(); + await app.confirmInputDialog("Warning: Only main.py downloaded"); + }); +}); diff --git a/src/e2e/settings.spec.ts b/src/e2e/settings.spec.ts index 26a70587f..412221d5d 100644 --- a/src/e2e/settings.spec.ts +++ b/src/e2e/settings.spec.ts @@ -9,19 +9,19 @@ test.describe("settings", () => { test("sets language via URL", async ({ app }) => { await app.goto({ language: "fr" }); // French via the URL - await app.expectProjectName("Projet sans titre"); + await app.findProjectName("Projet sans titre"); await app.switchLanguage("en"); await app.page.reload(); // French URL ignored as we've made an explicit language choice. - await app.expectProjectName("Untitled project"); + await app.findProjectName("Untitled project"); }); test("switches language", async ({ app }) => { await app.goto(); await app.switchLanguage("fr"); - await app.expectProjectName("Projet sans titre"); + await app.findProjectName("Projet sans titre"); await app.switchLanguage("en"); - await app.expectProjectName("Untitled project"); + await app.findProjectName("Untitled project"); }); }); From cdd14bf34a02bf78d4d1476ab45eafbbe6fd4582 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 15:37:50 +0000 Subject: [PATCH 08/38] Add rest of playwright tests --- playwright.config.ts | 7 +- src/e2e/app-playwright.ts | 264 ++++++++++++++++++++++++++++++++-- src/e2e/app-test-fixtures.ts | 26 +++- src/e2e/autocomplete.spec.ts | 103 +++++++++++++ src/e2e/connect.spec.ts | 94 ++++++++++++ src/e2e/documentation.spec.ts | 5 +- src/e2e/simulator.spec.ts | 61 ++++++++ 7 files changed, 539 insertions(+), 21 deletions(-) create mode 100644 src/e2e/autocomplete.spec.ts create mode 100644 src/e2e/connect.spec.ts create mode 100644 src/e2e/simulator.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 6da0e0d60..f8cc8a17c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -20,12 +20,7 @@ export default defineConfig({ projects: [ { name: "chromium", - use: { - ...devices["Desktop Chrome"], - launchOptions: { - channel: "chromium-tip-of-tree", - }, - }, + use: { ...devices["Desktop Chrome"] }, }, ], diff --git a/src/e2e/app-playwright.ts b/src/e2e/app-playwright.ts index 4ee01e743..ece1ae5cf 100644 --- a/src/e2e/app-playwright.ts +++ b/src/e2e/app-playwright.ts @@ -3,11 +3,12 @@ * * SPDX-License-Identifier: MIT */ -import { Locator, Page, expect } from "@playwright/test"; +import { BrowserContext, Frame, Locator, Page, expect } from "@playwright/test"; import { Flag } from "../flags"; import path from "path"; import { fileURLToPath } from "url"; import { readFileSync } from "fs"; +import { WebUSBErrorCode } from "../device/device"; export enum LoadDialogType { CONFIRM, @@ -122,9 +123,14 @@ export class App { private searchButton: Locator; public modifierKey: string; public projectTab: ProjectTabPanel; - - constructor(public readonly page: Page) { - this.editorTextArea = this.page.getByTestId("editor").getByRole("textbox"); + private moreConnectionOptionsButton: Locator; + public baseUrl: string; + private editor: Locator; + + constructor(public readonly page: Page, public context: BrowserContext) { + this.baseUrl = baseUrl; + this.editor = this.page.getByTestId("editor"); + this.editorTextArea = this.editor.getByRole("textbox"); this.projectTab = new ProjectTabPanel(page); this.settingsButton = this.page.getByTestId("settings"); this.saveButton = this.page.getByRole("button", { @@ -132,6 +138,11 @@ export class App { exact: true, }); this.searchButton = this.page.getByRole("button", { name: "Search" }); + this.moreConnectionOptionsButton = this.page.getByTestId( + "more-connect-options" + ); + + // Set modifier key const isMac = process.platform === "darwin"; this.modifierKey = isMac ? "Meta" : "Control"; } @@ -185,7 +196,11 @@ export class App { } async typeInEditor(text: string): Promise { - await this.editorTextArea.fill(text); + const textWithoutLastChar = text.slice(0, text.length - 1); + const lastChar = text.slice(-1); + await this.editorTextArea.fill(textWithoutLastChar); + // Last character is typed separately to trigger editor suggestions + await this.page.keyboard.press(lastChar); } async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { @@ -211,7 +226,7 @@ export class App { // Scroll to the top of code text area await this.editorTextArea.click(); await this.page.mouse.wheel(0, -100000000); - return expect(this.editorTextArea).toContainText(match); + await expect(this.editorTextArea).toContainText(match); } // TODO: Rename to expectProjectFiles @@ -307,8 +322,10 @@ export class App { } } - async closeDialog(dialogText: string) { - await this.page.getByText(dialogText).waitFor(); + async closeDialog(dialogText?: string) { + if (dialogText) { + await this.page.getByText(dialogText).waitFor(); + } await this.page.getByRole("button", { name: "Close" }).first().click(); } @@ -358,7 +375,7 @@ export class App { expect(dialog.type() === "beforeunload").toEqual(visible); await dialog.dismiss(); }); - await this.page.close({ runBeforeUnload: true }); + this.page.close({ runBeforeUnload: true }); } // Rename to expectDocumentationTopLevelHeading @@ -442,8 +459,237 @@ export class App { async selectDocumentationIdea(name: string): Promise { await this.page.getByRole("button", { name }).click(); } + + async connect(): Promise { + await this.moreConnectionOptionsButton.click(); + await this.page.getByRole("menuitem", { name: "Connect" }).click(); + await this.connectViaConnectHelp(); + } + + // Connects from the connect dialog/wizard. + async connectViaConnectHelp(): Promise { + await this.page.getByRole("button", { name: "Next" }).click(); + await this.page.getByRole("button", { name: "Next" }).click(); + } + + // TODO: Extract as variable instead of function + private findMainSerialArea() { + return this.page.getByRole("region", { + name: "Serial terminal", + exact: true, + }); + } + + async confirmConnection(): Promise { + const serialMenu = this.findMainSerialArea().getByRole("button", { + name: "Serial menu", + }); + await expect(serialMenu).toBeVisible(); + } + + // TODO: Move expect out to separate function + async disconnect(): Promise { + await this.moreConnectionOptionsButton.click(); + await this.page.getByRole("menuitem", { name: "Disconnect" }).click(); + const btns = await this.page + .getByRole("button", { name: "Serial terminal" }) + .all(); + + expect(btns.length).toEqual(0); + } + + async serialShow(): Promise { + await this.findMainSerialArea() + .getByRole("button", { name: "Show serial" }) + .click(); + + // TODO: Extract + // Make sure the button has flipped. + const hideSerialButton = this.findMainSerialArea().getByRole("button", { + name: "Hide serial", + }); + await expect(hideSerialButton).toBeVisible(); + } + + async serialHide(): Promise { + await this.findMainSerialArea() + .getByRole("button", { name: "Hide serial" }) + .click(); + + // TODO: Extract + // Make sure the button has flipped. + const showSerialButton = this.findMainSerialArea().getByRole("button", { + name: "Show serial", + }); + await expect(showSerialButton).toBeVisible(); + } + + async flash() { + await this.page.getByRole("button", { name: "Send to micro:bit" }).click(); + } + + async mockSerialWrite(data: string): Promise { + this.page.evaluate((data) => { + (window as any).mockDevice.mockSerialWrite(data); + }, toCrLf(data)); + } + + async followSerialCompactTracebackLink(): Promise { + await this.page.getByTestId("traceback-link").click(); + } + + async mockDeviceConnectFailure(code: WebUSBErrorCode) { + this.page.evaluate((code) => { + (window as any).mockDevice.mockConnect(code); + }, code); + } + + async findSerialCompactTraceback(text: string | RegExp): Promise { + await expect(this.page.getByText(text)).toBeVisible(); + } + + // Retry micro:bit connection from error dialogs. + async connectViaTryAgain(): Promise { + await this.page.getByRole("button", { name: "Try again" }).click(); + } + + // Launch 'connect help' dialog from 'not found' dialog. + async connectHelpFromNotFoundDialog(): Promise { + await this.page.getByRole("link", { name: "follow these steps" }).click(); + } + + async mockWebUsbNotSupported() { + this.page.evaluate(() => { + (window as any).mockDevice.mockWebUsbNotSupported(); + }); + } + + // TODO: Rename to expectCompletionOptions + async findCompletionOptions(expected: string[]): Promise { + const completions = this.page.getByRole("listbox", { name: "Completions" }); + const contents = await completions.innerText(); + + expect(contents).toEqual(expected.join("\n")); + } + + // TODO: Rename to expectCompletionActiveOption + async findCompletionActiveOption(signature: string): Promise { + const activeOption = this.editor + .locator("div") + .filter({ hasText: signature }) + .nth(2); + await expect(activeOption).toBeVisible(); + } + + async acceptCompletion(name: string): Promise { + // This seems significantly more reliable than pressing Enter, though there's + // no real-life issue here. + await this.editor.getByRole("option", { name }).click(); + } + + async followCompletionOrSignatureDocumentionLink( + linkName: "Help" | "API" + ): Promise { + await this.page.getByRole("link", { name: linkName }).click(); + } + + // TODO: Rename to expectActiveApiEntry + async findActiveApiEntry(text: string, _headingLevel: string): Promise { + // We need to make sure it's actually visible as it's scroll-based navigation. + await expect(this.page.getByRole("heading", { name: text })).toBeVisible(); + } + + // TODO: Rename to expectSignatureHelp + async findSignatureHelp(expectedSignature: string): Promise { + const signatureHelp = this.editor + .locator("div") + .filter({ hasText: expectedSignature }) + .nth(1); + await signatureHelp.waitFor(); + await expect(signatureHelp).toBeVisible(); + } + + // Simulator functions + private getSimulatorIframe(): Frame { + const simulatorIframe = this.page + .frames() + .find((frame) => frame.name() === "Simulator"); + if (!simulatorIframe) { + throw new Error("Simulator iframe not found"); + } + return simulatorIframe; + } + + async runSimulator(): Promise { + const simulatorIframe = this.getSimulatorIframe(); + const playButton = simulatorIframe.locator(".play-button"); + await playButton.click(); + } + + async simulatorSelectGesture(option: string): Promise { + await this.page + .getByTestId("simulator-gesture-select") + .selectOption(option); + } + + async simulatorSendGesture(): Promise { + await this.page.getByRole("button", { name: "Send gesture" }).click(); + } + + async simulatorConfirmResponse(): Promise { + // Confirms that top left LED is switched on + // to match Image.NO being displayed. + const gridLEDs = this.getSimulatorIframe().locator("#LEDsOn"); + await expect(gridLEDs).toBeVisible(); + } + + async simulatorSetRangeSlider( + sliderLabel: string, + value: "min" | "max" + ): Promise { + const sliderThumb = this.page.locator( + `[role="slider"][aria-label="${sliderLabel}"]` + ); + const bounding_box = await sliderThumb!.boundingBox(); + await this.page.mouse.move( + bounding_box!.x + bounding_box!.width / 2, + bounding_box!.y + bounding_box!.height / 2 + ); + await this.page.mouse.down(); + await this.page.waitForTimeout(500); + await this.page.mouse.move(value === "max" ? 1200 : 0, 0); + await this.page.waitForTimeout(500); + await this.page.mouse.up(); + } + + async simulatorInputPressHold( + name: string, + pressDuration: number + ): Promise { + const inputButton = this.page.getByRole("button", { + name, + }); + const bounding_box = await inputButton!.boundingBox(); + await this.page.mouse.move( + bounding_box!.x + bounding_box!.width / 2, + bounding_box!.y + bounding_box!.height / 2 + ); + await this.page.mouse.down(); + await this.page.waitForTimeout(pressDuration); + await this.page.mouse.up(); + } + + async findStoppedSimulator(): Promise { + const button = this.page.getByRole("button", { + name: "Stop simulator", + }); + expect(await button.isDisabled()).toEqual(true); + } } +const toCrLf = (text: string): string => + text.replace(/[\r\n]/g, "\n").replace(/\n/g, "\r\n"); + export const getFilename = (filePath: string) => { const filename = filePath.split("/").pop(); if (!filename) { diff --git a/src/e2e/app-test-fixtures.ts b/src/e2e/app-test-fixtures.ts index 565efe98b..b48071734 100644 --- a/src/e2e/app-test-fixtures.ts +++ b/src/e2e/app-test-fixtures.ts @@ -6,7 +6,29 @@ type MyFixtures = { }; export const test = base.extend({ - app: async ({ page }, use) => { - await use(new App(page)); + app: async ({ page, context }, use) => { + const app = new App(page, context); + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await context.addCookies([ + { + // See corresponding code in App.tsx. + name: "mockDevice", + value: "1", + url: app.baseUrl, + }, + // Don't show compliance notice for Foundation builds + { + name: "MBCC", + value: encodeURIComponent( + JSON.stringify({ + version: 1, + analytics: false, + functional: true, + }) + ), + url: app.baseUrl, + }, + ]); + await use(app); }, }); diff --git a/src/e2e/autocomplete.spec.ts b/src/e2e/autocomplete.spec.ts new file mode 100644 index 000000000..1630757fc --- /dev/null +++ b/src/e2e/autocomplete.spec.ts @@ -0,0 +1,103 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +const showFullSignature = + "show(image, delay=400, wait=True, loop=False, clear=False)"; + +test.describe("autocomplete", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("shows autocomplete as you type", async ({ app }) => { + await app.selectAllInEditor(); + await app.typeInEditor("from microbit import *\ndisplay.s"); + + // Initial completions + await app.findCompletionOptions(["scroll", "set_pixel", "show"]); + await app.findCompletionActiveOption("scroll(text)"); + + // Further refinement + await app.page.keyboard.press("h"); + await app.findCompletionActiveOption("show(image)"); + + // Accepted completion + await app.acceptCompletion("show"); + await app.findVisibleEditorContents("display.show()"); + }); + + test("ranks Image above image=", async ({ app }) => { + // This particular case has been tweaked in a somewhat fragile way. + // See the boost code in autocompletion.ts + + await app.selectAllInEditor(); + await app.typeInEditor("from microbit import *\ndisplay.show(image"); + + await app.findCompletionOptions(["Image", "image="]); + }); + + test("autocomplete can navigate to API toolkit content", async ({ app }) => { + await app.selectAllInEditor(); + await app.typeInEditor("from microbit import *\ndisplay.sho"); + + await app.findCompletionActiveOption("show(image)"); + + await app.followCompletionOrSignatureDocumentionLink("API"); + + await app.findActiveApiEntry(showFullSignature, "h4"); + }); + + test("autocomplete can navigate to Reference toolkit content", async ({ + app, + }) => { + await app.selectAllInEditor(); + await app.typeInEditor("from microbit import *\ndisplay.sho"); + await app.findCompletionActiveOption("show(image)"); + await app.followCompletionOrSignatureDocumentionLink("Help"); + await app.findActiveApiEntry("Show", "h3"); + }); + + test("shows signature help after autocomplete", async ({ app }) => { + await app.selectAllInEditor(); + await app.typeInEditor("from microbit import *\ndisplay.sho"); + await app.acceptCompletion("show"); + + await app.findSignatureHelp(showFullSignature); + }); + + test("does not insert brackets for import completion", async ({ app }) => { + // This relies on undocumented Pyright behaviour so important to cover at a high level. + await app.selectAllInEditor(); + await app.typeInEditor("from audio import is_pla"); + await app.acceptCompletion("is_playing"); + + await app.findVisibleEditorContents(/is_playing$/); + }); + + test("signature can navigate to API toolkit content", async ({ app }) => { + await app.selectAllInEditor(); + // The closing bracket is autoinserted. + await app.typeInEditor("from microbit import *\ndisplay.show("); + + await app.findSignatureHelp(showFullSignature); + + await app.followCompletionOrSignatureDocumentionLink("API"); + + await app.findActiveApiEntry(showFullSignature, "h4"); + }); + + test("signature can navigate to Reference toolkit content", async ({ + app, + }) => { + await app.selectAllInEditor(); + // The closing bracket is autoinserted. + await app.typeInEditor("from microbit import *\ndisplay.show("); + await app.findSignatureHelp(showFullSignature); + await app.followCompletionOrSignatureDocumentionLink("Help"); + await app.findActiveApiEntry("Show", "h3"); + }); +}); diff --git a/src/e2e/connect.spec.ts b/src/e2e/connect.spec.ts new file mode 100644 index 000000000..9dd704718 --- /dev/null +++ b/src/e2e/connect.spec.ts @@ -0,0 +1,94 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +const traceback = `Traceback (most recent call last): + File "main.py", line 6 +SyntaxError: invalid syntax +`; // Needs trailing newline! + +test.describe("connect", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("shows serial when connected", async ({ app }) => { + // Connect and disconnect wait for serial to be shown/hidden + await app.connect(); + await app.confirmConnection(); + await app.disconnect(); + }); + + test("can expand serial to show full output", async ({ app }) => { + await app.connect(); + + await app.serialShow(); + + await app.serialHide(); + }); + + test("shows summary of traceback from serial", async ({ app }) => { + await app.connect(); + await app.flash(); + await app.mockSerialWrite(traceback); + + await app.findSerialCompactTraceback(/SyntaxError: invalid syntax/); + }); + + test("supports navigating to line from traceback", async ({ app }) => { + await app.connect(); + await app.flash(); + await app.mockSerialWrite(traceback); + + await app.followSerialCompactTracebackLink(); + + // No good options yet for asserting editor line. + }); + + test("shows the micro:bit not found dialog and connects on try again", async ({ + app, + }) => { + await app.mockDeviceConnectFailure("no-device-selected"); + await app.connect(); + await app.confirmInputDialog("No micro:bit found"); + await app.connectViaTryAgain(); + await app.connectViaConnectHelp(); + await app.confirmConnection(); + }); + + test("shows the micro:bit not found dialog and connects after launching the connect help dialog", async ({ + app, + }) => { + await app.mockDeviceConnectFailure("no-device-selected"); + await app.connect(); + await app.confirmInputDialog("No micro:bit found"); + await app.connectHelpFromNotFoundDialog(); + await app.connectViaConnectHelp(); + await app.confirmConnection(); + }); + + test("shows the update firmware dialog and connects on try again", async ({ + app, + }) => { + await app.mockDeviceConnectFailure("update-req"); + await app.connect(); + await app.confirmInputDialog("Firmware update required"); + await app.connectViaTryAgain(); + await app.connectViaConnectHelp(); + await app.confirmConnection(); + }); + + test("Shows the transfer hex help dialog after send to micro:bit where WebUSB is not supported", async ({ + app, + }) => { + await app.mockWebUsbNotSupported(); + await app.setProjectName("not default name"); + await app.flash(); + await app.confirmInputDialog("This browser does not support WebUSB"); + await app.closeDialog(); + await app.confirmInputDialog("Transfer saved hex file to micro:bit"); + }); +}); diff --git a/src/e2e/documentation.spec.ts b/src/e2e/documentation.spec.ts index 9e10042b2..8b4ac501f 100644 --- a/src/e2e/documentation.spec.ts +++ b/src/e2e/documentation.spec.ts @@ -18,8 +18,7 @@ test.describe("documentation", () => { ); }); - test("Copy code and paste in editor", async ({ app, context }) => { - await context.grantPermissions(["clipboard-read", "clipboard-write"]); + test("Copy code and paste in editor", async ({ app }) => { const tab = "Reference"; await app.selectAllInEditor(); await app.typeInEditor("# Initial document"); @@ -34,9 +33,7 @@ test.describe("documentation", () => { test("Copy code after dropdown choice and paste in editor", async ({ app, - context, }) => { - await context.grantPermissions(["clipboard-read", "clipboard-write"]); const tab = "Reference"; await app.selectAllInEditor(); await app.typeInEditor("# Initial document"); diff --git a/src/e2e/simulator.spec.ts b/src/e2e/simulator.spec.ts new file mode 100644 index 000000000..ab5bb29b5 --- /dev/null +++ b/src/e2e/simulator.spec.ts @@ -0,0 +1,61 @@ +/** + * (c) 2021, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +const basicTest = "from microbit import *\ndisplay.show(Image.NO)"; + +const buttonTest = + "from microbit import *\nwhile True:\nif button_a.was_pressed():\ndisplay.show(Image.NO)"; + +const gestureTest = + "from microbit import *\nwhile True:\nif accelerometer.was_gesture('freefall'):\ndisplay.show(Image.NO)"; + +const sliderTest = + "from microbit import *\nwhile True:\nif temperature() == -5:\ndisplay.show(Image.NO)"; + +test.describe("simulator", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("responds to a sent gesture", async ({ app }) => { + // Enum sensor change via select and button. + await app.selectAllInEditor(); + await app.typeInEditor(gestureTest); + await app.runSimulator(); + await app.simulatorSelectGesture("freefall"); + await app.simulatorSendGesture(); + await app.simulatorConfirmResponse(); + }); + + test("responds to a range sensor change", async ({ app }) => { + // Range sensor change via slider. + await app.selectAllInEditor(); + await app.typeInEditor(sliderTest); + await app.runSimulator(); + await app.simulatorSetRangeSlider("Temperature", "min"); + await app.simulatorConfirmResponse(); + }); + + test("responds to a button press", async ({ app }) => { + // Range sensor change via button. + await app.selectAllInEditor(); + await app.typeInEditor(buttonTest); + await app.runSimulator(); + await app.simulatorInputPressHold("Press button A", 500); + await app.simulatorConfirmResponse(); + }); + test("stops when the code changes", async ({ app }) => { + await app.selectAllInEditor(); + await app.typeInEditor(basicTest); + await app.runSimulator(); + await app.simulatorConfirmResponse(); + + await app.typeInEditor("A change!"); + + await app.findStoppedSimulator(); + }); +}); From c7a9bc01dfa32284aaa4b37cfc622830788249fd Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 16:41:01 +0000 Subject: [PATCH 09/38] Add accessibility.spec.ts --- src/e2e/accessibility.spec.ts | 44 ++++++++++++++++ src/e2e/app-playwright.ts | 97 +++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/e2e/accessibility.spec.ts diff --git a/src/e2e/accessibility.spec.ts b/src/e2e/accessibility.spec.ts new file mode 100644 index 000000000..2e4257405 --- /dev/null +++ b/src/e2e/accessibility.spec.ts @@ -0,0 +1,44 @@ +/** + * (c) 2022, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { test } from "./app-test-fixtures.js"; + +test.describe("accessibility", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); + + test("focuses the correct element on tabbing after load", async ({ app }) => { + await app.assertFocusOnLoad(); + }); + + test("focuses the correct elements on collapsing and expanding the simulator", async ({ + app, + }) => { + await app.collapseSimulator(); + await app.assertFocusOnExpandSimulator(); + + await app.expandSimulator(); + await app.assertFocusOnSimulator(); + }); + + test("focuses the correct elements on collapsing and expanding the sidebar", async ({ + app, + }) => { + await app.expandSidebar(); + await app.assertFocusOnSidebar(); + + await app.collapseSidebar(); + await app.assertFocusOnExpandSidebar(); + }); + + test("allows tab out of editor", async ({ app }) => { + await app.tabOutOfEditorForwards(); + await app.assertFocusAfterEditor(); + + await app.tabOutOfEditorBackwards(); + await app.assertFocusBeforeEditor(); + }); +}); diff --git a/src/e2e/app-playwright.ts b/src/e2e/app-playwright.ts index ece1ae5cf..00f83cca1 100644 --- a/src/e2e/app-playwright.ts +++ b/src/e2e/app-playwright.ts @@ -116,6 +116,26 @@ class ProjectTabPanel { } } +class SideBar { + public expandButton: Locator; + public collapseButton: Locator; + + constructor(public readonly page: Page) { + this.expandButton = this.page.getByLabel("Expand sidebar"); + this.collapseButton = this.page.getByLabel("Collapse sidebar"); + } +} + +class Simulator { + public expandButton: Locator; + public collapseButton: Locator; + + constructor(public readonly page: Page) { + this.expandButton = this.page.getByLabel("Expand simulator"); + this.collapseButton = this.page.getByLabel("Collapse simulator"); + } +} + export class App { public editorTextArea: Locator; private settingsButton: Locator; @@ -126,6 +146,8 @@ export class App { private moreConnectionOptionsButton: Locator; public baseUrl: string; private editor: Locator; + public simulator: Simulator; + public sidebar: SideBar; constructor(public readonly page: Page, public context: BrowserContext) { this.baseUrl = baseUrl; @@ -141,6 +163,8 @@ export class App { this.moreConnectionOptionsButton = this.page.getByTestId( "more-connect-options" ); + this.simulator = new Simulator(this.page); + this.sidebar = new SideBar(this.page); // Set modifier key const isMac = process.platform === "darwin"; @@ -149,6 +173,8 @@ export class App { async goto(options: UrlOptions = {}) { await this.page.goto(optionsToURL(options)); + // Wait for the page to be loaded + await this.editor.waitFor(); } // TODO: Rename to expectProjectName @@ -685,6 +711,77 @@ export class App { }); expect(await button.isDisabled()).toEqual(true); } + + // TODO: Rename to expectFocusOnLoad + async assertFocusOnLoad(): Promise { + const link = this.page.getByLabel( + "visit microbit.org (opens in a new tab)" + ); + await this.page.keyboard.press("Tab"); + await expect(link).toBeFocused(); + } + + async collapseSimulator(): Promise { + await this.simulator.collapseButton.click(); + } + + async expandSimulator(): Promise { + await this.simulator.expandButton.click(); + } + + async collapseSidebar(): Promise { + await this.sidebar.collapseButton.click(); + } + + async expandSidebar(): Promise { + await this.sidebar.expandButton.click(); + } + + async assertFocusOnExpandSimulator(): Promise { + await expect(this.simulator.expandButton).toBeFocused(); + } + + async assertFocusOnSimulator(): Promise { + const simulator = this.page.locator("iframe[name='Simulator']"); + await expect(simulator).toBeFocused(); + } + + async assertFocusOnExpandSidebar(): Promise { + await expect(this.sidebar.expandButton).toBeFocused(); + } + + async assertFocusOnSidebar(): Promise { + const simulator = this.page.getByRole("tabpanel", { name: "Reference" }); + await expect(simulator).toBeFocused(); + } + + async assertFocusBeforeEditor(): Promise { + const zoomIn = this.page.getByRole("button", { + name: "Zoom in", + }); + await expect(zoomIn).toBeFocused(); + } + + async assertFocusAfterEditor(): Promise { + const sendButton = this.page.getByRole("button", { + name: "Send to micro:bit", + }); + await expect(sendButton).toBeFocused(); + } + + async tabOutOfEditorForwards(): Promise { + await this.editor.click(); + await this.page.keyboard.press("Escape"); + await this.page.keyboard.press("Tab"); + } + + async tabOutOfEditorBackwards(): Promise { + await this.editor.click(); + await this.page.keyboard.press("Escape"); + await this.page.keyboard.down("Shift"); + await this.page.keyboard.press("Tab"); + await this.page.keyboard.up("Shift"); + } } const toCrLf = (text: string): string => From 3c61a54c44ec76a153c1354b367de9f8ad226b67 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 16:50:52 +0000 Subject: [PATCH 10/38] Rename *.spec.ts -> *.test.ts --- src/e2e/accessibility.spec.ts | 44 ---------- src/e2e/accessibility.test.ts | 29 ++++--- src/e2e/autocomplete.spec.ts | 103 ----------------------- src/e2e/autocomplete.test.ts | 34 ++++---- src/e2e/connect.spec.ts | 94 --------------------- src/e2e/connect.test.ts | 45 +++++----- src/e2e/documentation.spec.ts | 92 -------------------- src/e2e/documentation.test.ts | 43 ++++------ src/e2e/edits.spec.ts | 40 --------- src/e2e/edits.test.ts | 28 +++---- src/e2e/migration.spec.ts | 32 ------- src/e2e/migration.test.ts | 22 ++--- src/e2e/multiple-files.spec.ts | 90 -------------------- src/e2e/multiple-files.test.ts | 30 +++---- src/e2e/open.spec.ts | 149 --------------------------------- src/e2e/open.test.ts | 51 ++++++----- src/e2e/reset.spec.ts | 23 ----- src/e2e/reset.test.ts | 12 +-- src/e2e/save.spec.ts | 73 ---------------- src/e2e/save.test.ts | 53 ++++++++---- src/e2e/settings.spec.ts | 27 ------ src/e2e/settings.test.ts | 25 ++---- src/e2e/simulator.spec.ts | 61 -------------- src/e2e/simulator.test.ts | 19 ++--- 24 files changed, 200 insertions(+), 1019 deletions(-) delete mode 100644 src/e2e/accessibility.spec.ts delete mode 100644 src/e2e/autocomplete.spec.ts delete mode 100644 src/e2e/connect.spec.ts delete mode 100644 src/e2e/documentation.spec.ts delete mode 100644 src/e2e/edits.spec.ts delete mode 100644 src/e2e/migration.spec.ts delete mode 100644 src/e2e/multiple-files.spec.ts delete mode 100644 src/e2e/open.spec.ts delete mode 100644 src/e2e/reset.spec.ts delete mode 100644 src/e2e/save.spec.ts delete mode 100644 src/e2e/settings.spec.ts delete mode 100644 src/e2e/simulator.spec.ts diff --git a/src/e2e/accessibility.spec.ts b/src/e2e/accessibility.spec.ts deleted file mode 100644 index 2e4257405..000000000 --- a/src/e2e/accessibility.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * (c) 2022, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -test.describe("accessibility", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("focuses the correct element on tabbing after load", async ({ app }) => { - await app.assertFocusOnLoad(); - }); - - test("focuses the correct elements on collapsing and expanding the simulator", async ({ - app, - }) => { - await app.collapseSimulator(); - await app.assertFocusOnExpandSimulator(); - - await app.expandSimulator(); - await app.assertFocusOnSimulator(); - }); - - test("focuses the correct elements on collapsing and expanding the sidebar", async ({ - app, - }) => { - await app.expandSidebar(); - await app.assertFocusOnSidebar(); - - await app.collapseSidebar(); - await app.assertFocusOnExpandSidebar(); - }); - - test("allows tab out of editor", async ({ app }) => { - await app.tabOutOfEditorForwards(); - await app.assertFocusAfterEditor(); - - await app.tabOutOfEditorBackwards(); - await app.assertFocusBeforeEditor(); - }); -}); diff --git a/src/e2e/accessibility.test.ts b/src/e2e/accessibility.test.ts index cad343f4f..2e4257405 100644 --- a/src/e2e/accessibility.test.ts +++ b/src/e2e/accessibility.test.ts @@ -3,19 +3,20 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; -describe("accessibility", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("accessibility", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("focuses the correct element on tabbing after load", async () => { + test("focuses the correct element on tabbing after load", async ({ app }) => { await app.assertFocusOnLoad(); }); - it("focuses the correct elements on collapsing and expanding the simulator", async () => { + test("focuses the correct elements on collapsing and expanding the simulator", async ({ + app, + }) => { await app.collapseSimulator(); await app.assertFocusOnExpandSimulator(); @@ -23,15 +24,17 @@ describe("accessibility", () => { await app.assertFocusOnSimulator(); }); - it("focuses the correct elements on collapsing and expanding the sidebar", async () => { - await app.collapseSidebar(); - await app.assertFocusOnExpandSidebar(); - + test("focuses the correct elements on collapsing and expanding the sidebar", async ({ + app, + }) => { await app.expandSidebar(); await app.assertFocusOnSidebar(); + + await app.collapseSidebar(); + await app.assertFocusOnExpandSidebar(); }); - it("allows tab out of editor", async () => { + test("allows tab out of editor", async ({ app }) => { await app.tabOutOfEditorForwards(); await app.assertFocusAfterEditor(); diff --git a/src/e2e/autocomplete.spec.ts b/src/e2e/autocomplete.spec.ts deleted file mode 100644 index 1630757fc..000000000 --- a/src/e2e/autocomplete.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -const showFullSignature = - "show(image, delay=400, wait=True, loop=False, clear=False)"; - -test.describe("autocomplete", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("shows autocomplete as you type", async ({ app }) => { - await app.selectAllInEditor(); - await app.typeInEditor("from microbit import *\ndisplay.s"); - - // Initial completions - await app.findCompletionOptions(["scroll", "set_pixel", "show"]); - await app.findCompletionActiveOption("scroll(text)"); - - // Further refinement - await app.page.keyboard.press("h"); - await app.findCompletionActiveOption("show(image)"); - - // Accepted completion - await app.acceptCompletion("show"); - await app.findVisibleEditorContents("display.show()"); - }); - - test("ranks Image above image=", async ({ app }) => { - // This particular case has been tweaked in a somewhat fragile way. - // See the boost code in autocompletion.ts - - await app.selectAllInEditor(); - await app.typeInEditor("from microbit import *\ndisplay.show(image"); - - await app.findCompletionOptions(["Image", "image="]); - }); - - test("autocomplete can navigate to API toolkit content", async ({ app }) => { - await app.selectAllInEditor(); - await app.typeInEditor("from microbit import *\ndisplay.sho"); - - await app.findCompletionActiveOption("show(image)"); - - await app.followCompletionOrSignatureDocumentionLink("API"); - - await app.findActiveApiEntry(showFullSignature, "h4"); - }); - - test("autocomplete can navigate to Reference toolkit content", async ({ - app, - }) => { - await app.selectAllInEditor(); - await app.typeInEditor("from microbit import *\ndisplay.sho"); - await app.findCompletionActiveOption("show(image)"); - await app.followCompletionOrSignatureDocumentionLink("Help"); - await app.findActiveApiEntry("Show", "h3"); - }); - - test("shows signature help after autocomplete", async ({ app }) => { - await app.selectAllInEditor(); - await app.typeInEditor("from microbit import *\ndisplay.sho"); - await app.acceptCompletion("show"); - - await app.findSignatureHelp(showFullSignature); - }); - - test("does not insert brackets for import completion", async ({ app }) => { - // This relies on undocumented Pyright behaviour so important to cover at a high level. - await app.selectAllInEditor(); - await app.typeInEditor("from audio import is_pla"); - await app.acceptCompletion("is_playing"); - - await app.findVisibleEditorContents(/is_playing$/); - }); - - test("signature can navigate to API toolkit content", async ({ app }) => { - await app.selectAllInEditor(); - // The closing bracket is autoinserted. - await app.typeInEditor("from microbit import *\ndisplay.show("); - - await app.findSignatureHelp(showFullSignature); - - await app.followCompletionOrSignatureDocumentionLink("API"); - - await app.findActiveApiEntry(showFullSignature, "h4"); - }); - - test("signature can navigate to Reference toolkit content", async ({ - app, - }) => { - await app.selectAllInEditor(); - // The closing bracket is autoinserted. - await app.typeInEditor("from microbit import *\ndisplay.show("); - await app.findSignatureHelp(showFullSignature); - await app.followCompletionOrSignatureDocumentionLink("Help"); - await app.findActiveApiEntry("Show", "h3"); - }); -}); diff --git a/src/e2e/autocomplete.test.ts b/src/e2e/autocomplete.test.ts index a6683abce..1630757fc 100644 --- a/src/e2e/autocomplete.test.ts +++ b/src/e2e/autocomplete.test.ts @@ -3,19 +3,17 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; const showFullSignature = "show(image, delay=400, wait=True, loop=False, clear=False)"; -describe("autocomplete", () => { - // Enable flags to allow testing the toolkit interactions. - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("autocomplete", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("shows autocomplete as you type", async () => { + test("shows autocomplete as you type", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.s"); @@ -24,7 +22,7 @@ describe("autocomplete", () => { await app.findCompletionActiveOption("scroll(text)"); // Further refinement - await app.typeInEditor("h"); + await app.page.keyboard.press("h"); await app.findCompletionActiveOption("show(image)"); // Accepted completion @@ -32,7 +30,7 @@ describe("autocomplete", () => { await app.findVisibleEditorContents("display.show()"); }); - it("ranks Image above image=", async () => { + test("ranks Image above image=", async ({ app }) => { // This particular case has been tweaked in a somewhat fragile way. // See the boost code in autocompletion.ts @@ -42,7 +40,7 @@ describe("autocomplete", () => { await app.findCompletionOptions(["Image", "image="]); }); - it("autocomplete can navigate to API toolkit content", async () => { + test("autocomplete can navigate to API toolkit content", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.sho"); @@ -53,7 +51,9 @@ describe("autocomplete", () => { await app.findActiveApiEntry(showFullSignature, "h4"); }); - it("autocomplete can navigate to Reference toolkit content", async () => { + test("autocomplete can navigate to Reference toolkit content", async ({ + app, + }) => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.sho"); await app.findCompletionActiveOption("show(image)"); @@ -61,7 +61,7 @@ describe("autocomplete", () => { await app.findActiveApiEntry("Show", "h3"); }); - it("shows signature help after autocomplete", async () => { + test("shows signature help after autocomplete", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.sho"); await app.acceptCompletion("show"); @@ -69,7 +69,7 @@ describe("autocomplete", () => { await app.findSignatureHelp(showFullSignature); }); - it("does not insert brackets for import completion", async () => { + test("does not insert brackets for import completion", async ({ app }) => { // This relies on undocumented Pyright behaviour so important to cover at a high level. await app.selectAllInEditor(); await app.typeInEditor("from audio import is_pla"); @@ -78,7 +78,7 @@ describe("autocomplete", () => { await app.findVisibleEditorContents(/is_playing$/); }); - it("signature can navigate to API toolkit content", async () => { + test("signature can navigate to API toolkit content", async ({ app }) => { await app.selectAllInEditor(); // The closing bracket is autoinserted. await app.typeInEditor("from microbit import *\ndisplay.show("); @@ -90,7 +90,9 @@ describe("autocomplete", () => { await app.findActiveApiEntry(showFullSignature, "h4"); }); - it("signature can navigate to Reference toolkit content", async () => { + test("signature can navigate to Reference toolkit content", async ({ + app, + }) => { await app.selectAllInEditor(); // The closing bracket is autoinserted. await app.typeInEditor("from microbit import *\ndisplay.show("); diff --git a/src/e2e/connect.spec.ts b/src/e2e/connect.spec.ts deleted file mode 100644 index 9dd704718..000000000 --- a/src/e2e/connect.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -const traceback = `Traceback (most recent call last): - File "main.py", line 6 -SyntaxError: invalid syntax -`; // Needs trailing newline! - -test.describe("connect", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("shows serial when connected", async ({ app }) => { - // Connect and disconnect wait for serial to be shown/hidden - await app.connect(); - await app.confirmConnection(); - await app.disconnect(); - }); - - test("can expand serial to show full output", async ({ app }) => { - await app.connect(); - - await app.serialShow(); - - await app.serialHide(); - }); - - test("shows summary of traceback from serial", async ({ app }) => { - await app.connect(); - await app.flash(); - await app.mockSerialWrite(traceback); - - await app.findSerialCompactTraceback(/SyntaxError: invalid syntax/); - }); - - test("supports navigating to line from traceback", async ({ app }) => { - await app.connect(); - await app.flash(); - await app.mockSerialWrite(traceback); - - await app.followSerialCompactTracebackLink(); - - // No good options yet for asserting editor line. - }); - - test("shows the micro:bit not found dialog and connects on try again", async ({ - app, - }) => { - await app.mockDeviceConnectFailure("no-device-selected"); - await app.connect(); - await app.confirmInputDialog("No micro:bit found"); - await app.connectViaTryAgain(); - await app.connectViaConnectHelp(); - await app.confirmConnection(); - }); - - test("shows the micro:bit not found dialog and connects after launching the connect help dialog", async ({ - app, - }) => { - await app.mockDeviceConnectFailure("no-device-selected"); - await app.connect(); - await app.confirmInputDialog("No micro:bit found"); - await app.connectHelpFromNotFoundDialog(); - await app.connectViaConnectHelp(); - await app.confirmConnection(); - }); - - test("shows the update firmware dialog and connects on try again", async ({ - app, - }) => { - await app.mockDeviceConnectFailure("update-req"); - await app.connect(); - await app.confirmInputDialog("Firmware update required"); - await app.connectViaTryAgain(); - await app.connectViaConnectHelp(); - await app.confirmConnection(); - }); - - test("Shows the transfer hex help dialog after send to micro:bit where WebUSB is not supported", async ({ - app, - }) => { - await app.mockWebUsbNotSupported(); - await app.setProjectName("not default name"); - await app.flash(); - await app.confirmInputDialog("This browser does not support WebUSB"); - await app.closeDialog(); - await app.confirmInputDialog("Transfer saved hex file to micro:bit"); - }); -}); diff --git a/src/e2e/connect.test.ts b/src/e2e/connect.test.ts index 0f8f503dc..9dd704718 100644 --- a/src/e2e/connect.test.ts +++ b/src/e2e/connect.test.ts @@ -3,27 +3,26 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; const traceback = `Traceback (most recent call last): File "main.py", line 6 SyntaxError: invalid syntax `; // Needs trailing newline! -describe("connect", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("connect", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("shows serial when connected", async () => { + test("shows serial when connected", async ({ app }) => { // Connect and disconnect wait for serial to be shown/hidden await app.connect(); await app.confirmConnection(); await app.disconnect(); }); - it("can expand serial to show full output", async () => { + test("can expand serial to show full output", async ({ app }) => { await app.connect(); await app.serialShow(); @@ -31,7 +30,7 @@ describe("connect", () => { await app.serialHide(); }); - it("shows summary of traceback from serial", async () => { + test("shows summary of traceback from serial", async ({ app }) => { await app.connect(); await app.flash(); await app.mockSerialWrite(traceback); @@ -39,7 +38,7 @@ describe("connect", () => { await app.findSerialCompactTraceback(/SyntaxError: invalid syntax/); }); - it("supports navigating to line from traceback", async () => { + test("supports navigating to line from traceback", async ({ app }) => { await app.connect(); await app.flash(); await app.mockSerialWrite(traceback); @@ -49,39 +48,47 @@ describe("connect", () => { // No good options yet for asserting editor line. }); - it("shows the micro:bit not found dialog and connects on try again", async () => { + test("shows the micro:bit not found dialog and connects on try again", async ({ + app, + }) => { await app.mockDeviceConnectFailure("no-device-selected"); await app.connect(); - await app.confirmGenericDialog("No micro:bit found"); + await app.confirmInputDialog("No micro:bit found"); await app.connectViaTryAgain(); await app.connectViaConnectHelp(); await app.confirmConnection(); }); - it("shows the micro:bit not found dialog and connects after launching the connect help dialog", async () => { + test("shows the micro:bit not found dialog and connects after launching the connect help dialog", async ({ + app, + }) => { await app.mockDeviceConnectFailure("no-device-selected"); await app.connect(); - await app.confirmGenericDialog("No micro:bit found"); + await app.confirmInputDialog("No micro:bit found"); await app.connectHelpFromNotFoundDialog(); await app.connectViaConnectHelp(); await app.confirmConnection(); }); - it("shows the update firmware dialog and connects on try again", async () => { + test("shows the update firmware dialog and connects on try again", async ({ + app, + }) => { await app.mockDeviceConnectFailure("update-req"); await app.connect(); - await app.confirmGenericDialog("Firmware update required"); + await app.confirmInputDialog("Firmware update required"); await app.connectViaTryAgain(); await app.connectViaConnectHelp(); await app.confirmConnection(); }); - it("Shows the transfer hex help dialog after send to micro:bit where WebUSB is not supported", async () => { + test("Shows the transfer hex help dialog after send to micro:bit where WebUSB is not supported", async ({ + app, + }) => { await app.mockWebUsbNotSupported(); await app.setProjectName("not default name"); await app.flash(); - await app.confirmGenericDialog("This browser does not support WebUSB"); + await app.confirmInputDialog("This browser does not support WebUSB"); await app.closeDialog(); - await app.confirmGenericDialog("Transfer saved hex file to micro:bit"); + await app.confirmInputDialog("Transfer saved hex file to micro:bit"); }); }); diff --git a/src/e2e/documentation.spec.ts b/src/e2e/documentation.spec.ts deleted file mode 100644 index 8b4ac501f..000000000 --- a/src/e2e/documentation.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -test.describe("documentation", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("API toolkit navigation", async ({ app }) => { - await app.switchTab("API"); - await app.findDocumentationTopLevelHeading( - "API", - "For usage and examples, see" - ); - }); - - test("Copy code and paste in editor", async ({ app }) => { - const tab = "Reference"; - await app.selectAllInEditor(); - await app.typeInEditor("# Initial document"); - await app.switchTab(tab); - await app.selectDocumentationSection("Display"); - await app.triggerScroll(tab); - await app.toggleCodeActionButton("Images: built-in"); - await app.copyCode("Images: built-in"); - await app.pasteToolkitCode(); - await app.findVisibleEditorContents("display.show(Image.HEART)"); - }); - - test("Copy code after dropdown choice and paste in editor", async ({ - app, - }) => { - const tab = "Reference"; - await app.selectAllInEditor(); - await app.typeInEditor("# Initial document"); - await app.switchTab(tab); - await app.selectDocumentationSection("Display"); - await app.triggerScroll(tab); - await app.selectToolkitDropDownOption( - "Select image:", - "silly" // "Image.SILLY" - ); - await app.toggleCodeActionButton("Images: built-in"); - await app.copyCode("Images: built-in"); - - await app.pasteToolkitCode(); - await app.findVisibleEditorContents("display.show(Image.SILLY)"); - }); - - test("Insert code via drag and drop", async ({ app }) => { - await app.selectAllInEditor(); - await app.typeInEditor("#1\n#2\n#3\n"); - await app.findVisibleEditorContents("#2"); - await app.switchTab("Reference"); - await app.selectDocumentationSection("Display"); - await app.dragDropCodeEmbed("Scroll", 2); - - // There's some weird trailing whitespace in this snippet that needs fixing in the content. - const expected = - "from microbit import *display.scroll('score') display.scroll(23)#1#2#3"; - - await app.findVisibleEditorContents(expected); - }); - - test("Searches and navigates to the first result", async ({ app }) => { - await app.searchToolkits("loop"); - await app.selectFirstSearchResult(); - await app.findDocumentationTopLevelHeading( - "Loops", - "Count and repeat sets of instructions" - ); - }); - - test("Ideas tab navigation", async ({ app }) => { - await app.switchTab("Ideas"); - await app.findDocumentationTopLevelHeading( - "Ideas", - "Try out these projects, modify them and get inspired" - ); - }); - - test("Select an idea", async ({ app }) => { - const ideaName = "Emotion badge"; - await app.switchTab("Ideas"); - await app.selectDocumentationIdea(ideaName); - await app.findDocumentationTopLevelHeading(ideaName); - }); -}); diff --git a/src/e2e/documentation.test.ts b/src/e2e/documentation.test.ts index db7274bff..8b4ac501f 100644 --- a/src/e2e/documentation.test.ts +++ b/src/e2e/documentation.test.ts @@ -3,15 +3,14 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; -describe("documentation", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("documentation", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("API toolkit navigation", async () => { + test("API toolkit navigation", async ({ app }) => { await app.switchTab("API"); await app.findDocumentationTopLevelHeading( "API", @@ -19,11 +18,7 @@ describe("documentation", () => { ); }); - it("Copy code and paste in editor", async () => { - if (process.platform === "darwin") { - // pasteToolkitCode doesn't work on Mac - return; - } + test("Copy code and paste in editor", async ({ app }) => { const tab = "Reference"; await app.selectAllInEditor(); await app.typeInEditor("# Initial document"); @@ -31,16 +26,14 @@ describe("documentation", () => { await app.selectDocumentationSection("Display"); await app.triggerScroll(tab); await app.toggleCodeActionButton("Images: built-in"); - await app.copyCode(); + await app.copyCode("Images: built-in"); await app.pasteToolkitCode(); await app.findVisibleEditorContents("display.show(Image.HEART)"); }); - it("Copy code after dropdown choice and paste in editor", async () => { - if (process.platform === "darwin") { - // pasteToolkitCode doesn't work on Mac - return; - } + test("Copy code after dropdown choice and paste in editor", async ({ + app, + }) => { const tab = "Reference"; await app.selectAllInEditor(); await app.typeInEditor("# Initial document"); @@ -52,28 +45,28 @@ describe("documentation", () => { "silly" // "Image.SILLY" ); await app.toggleCodeActionButton("Images: built-in"); - await app.copyCode(); + await app.copyCode("Images: built-in"); + await app.pasteToolkitCode(); await app.findVisibleEditorContents("display.show(Image.SILLY)"); }); - it("Insert code via drag and drop", async () => { + test("Insert code via drag and drop", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor("#1\n#2\n#3\n"); await app.findVisibleEditorContents("#2"); await app.switchTab("Reference"); await app.selectDocumentationSection("Display"); - await app.dragDropCodeEmbed("Scroll", 2); // There's some weird trailing whitespace in this snippet that needs fixing in the content. const expected = - "from microbit import *\n\n\ndisplay.scroll('score') \ndisplay.scroll(23)\n#1\n#2\n#3\n"; + "from microbit import *display.scroll('score') display.scroll(23)#1#2#3"; await app.findVisibleEditorContents(expected); }); - it("Searches and navigates to the first result", async () => { + test("Searches and navigates to the first result", async ({ app }) => { await app.searchToolkits("loop"); await app.selectFirstSearchResult(); await app.findDocumentationTopLevelHeading( @@ -82,7 +75,7 @@ describe("documentation", () => { ); }); - it("Ideas tab navigation", async () => { + test("Ideas tab navigation", async ({ app }) => { await app.switchTab("Ideas"); await app.findDocumentationTopLevelHeading( "Ideas", @@ -90,7 +83,7 @@ describe("documentation", () => { ); }); - it("Select an idea", async () => { + test("Select an idea", async ({ app }) => { const ideaName = "Emotion badge"; await app.switchTab("Ideas"); await app.selectDocumentationIdea(ideaName); diff --git a/src/e2e/edits.spec.ts b/src/e2e/edits.spec.ts deleted file mode 100644 index efa202a83..000000000 --- a/src/e2e/edits.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -test.describe("edits", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("doesn't prompt on close if no edits made", async ({ app }) => { - await app.closePageCheckDialog(false); - }); - - test("prompts on close if file edited", async ({ app }) => { - await app.typeInEditor("A change!"); - await app.findVisibleEditorContents(/A change/); - - await app.closePageCheckDialog(true); - }); - - test("prompts on close if project name edited", async ({ app }) => { - const name = "idiosyncratic ruminant"; - await app.setProjectName(name); - await app.findProjectName(name); - - await app.closePageCheckDialog(true); - }); - - test("retains text across a reload via session storage", async ({ app }) => { - await app.typeInEditor("A change!"); - await app.findVisibleEditorContents(/A change/); - - await app.page.reload(); - - await app.findVisibleEditorContents(/A change/); - }); -}); diff --git a/src/e2e/edits.test.ts b/src/e2e/edits.test.ts index 834a1dc2c..efa202a83 100644 --- a/src/e2e/edits.test.ts +++ b/src/e2e/edits.test.ts @@ -3,39 +3,37 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; -describe("edits", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - // We intentionally close the page so can't screenshot here. - // afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("edits", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("doesn't prompt on close if no edits made", async () => { - expect(await app.closePageCheckDialog()).toEqual(false); + test("doesn't prompt on close if no edits made", async ({ app }) => { + await app.closePageCheckDialog(false); }); - it("prompts on close if file edited", async () => { + test("prompts on close if file edited", async ({ app }) => { await app.typeInEditor("A change!"); await app.findVisibleEditorContents(/A change/); - expect(await app.closePageCheckDialog()).toEqual(true); + await app.closePageCheckDialog(true); }); - it("prompts on close if project name edited", async () => { + test("prompts on close if project name edited", async ({ app }) => { const name = "idiosyncratic ruminant"; await app.setProjectName(name); await app.findProjectName(name); - expect(await app.closePageCheckDialog()).toEqual(true); + await app.closePageCheckDialog(true); }); - it("retains text across a reload via session storage", async () => { + test("retains text across a reload via session storage", async ({ app }) => { await app.typeInEditor("A change!"); await app.findVisibleEditorContents(/A change/); - await app.reloadPage(); + await app.page.reload(); await app.findVisibleEditorContents(/A change/); }); diff --git a/src/e2e/migration.spec.ts b/src/e2e/migration.spec.ts deleted file mode 100644 index e0acccb20..000000000 --- a/src/e2e/migration.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -const heartMigrationFragment = - "#project:XQAAgACRAAAAAAAAAAA9iImmlGSt1R++5LD+ZJ36cRz46B+lhYtNRoWF0nijpaVyZlK7ACfSpeoQpgfk21st4ty06R4PEOM4sSAXBT95G3en+tghrYmE+YJp6EiYgzA9ThKkyShWq2UdvmCzqxoNfYc1wlmTqlNv/Piaz3WoSe3flvr/ItyLl0aolQlEpv4LA8A="; - -const sunlightSensorMigrationFragment = - "#project:XQAAgAByAQAAAAAAAAA9iImmlGSt1R++5LD+ZJ36cRz46B+lhYtNRoWF0nijpaVyZlK7ACfSpeoQpgfk21st4ty06R4PEOW6kOsIEMK7SL0Qco7jgsHFKZXfjv/XcHWvXG9qyz1a/a3NUulFDj/FDJxVAIV+WZLpRoo4E6MbW70FOgIfBPWP2hDVsojpoLc7ZfKI8SHxv54FSfB5bkbzaAKO+8CO73t6Odtv691JGjJ9MExFighY6GxyM/DoNInDDpAjFeaqCWrYdwENX7ZVM3we8f4swI71tL28N7sg588aB//A78AA"; - -test.describe("migration", () => { - test("Loads the project from the URL", async ({ app }) => { - await app.goto({ fragment: heartMigrationFragment }); - await app.findProjectName("Hearts"); - await app.findVisibleEditorContents( - "from microbit import *display.show(Image.HEART)" - ); - - // Regression test: Check that we can switch to a different migration in the same session. - // Previously we ignored the migration because we already had content in session storage. - await app.goto({ - fragment: sunlightSensorMigrationFragment, - }); - await app.page.reload(); - // wait for page to load - await app.saveButton.waitFor(); - await app.findVisibleEditorContents("display.read_light_level"); - }); -}); diff --git a/src/e2e/migration.test.ts b/src/e2e/migration.test.ts index f07e34211..e0acccb20 100644 --- a/src/e2e/migration.test.ts +++ b/src/e2e/migration.test.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; const heartMigrationFragment = "#project:XQAAgACRAAAAAAAAAAA9iImmlGSt1R++5LD+ZJ36cRz46B+lhYtNRoWF0nijpaVyZlK7ACfSpeoQpgfk21st4ty06R4PEOM4sSAXBT95G3en+tghrYmE+YJp6EiYgzA9ThKkyShWq2UdvmCzqxoNfYc1wlmTqlNv/Piaz3WoSe3flvr/ItyLl0aolQlEpv4LA8A="; @@ -11,26 +11,22 @@ const heartMigrationFragment = const sunlightSensorMigrationFragment = "#project:XQAAgAByAQAAAAAAAAA9iImmlGSt1R++5LD+ZJ36cRz46B+lhYtNRoWF0nijpaVyZlK7ACfSpeoQpgfk21st4ty06R4PEOW6kOsIEMK7SL0Qco7jgsHFKZXfjv/XcHWvXG9qyz1a/a3NUulFDj/FDJxVAIV+WZLpRoo4E6MbW70FOgIfBPWP2hDVsojpoLc7ZfKI8SHxv54FSfB5bkbzaAKO+8CO73t6Odtv691JGjJ9MExFighY6GxyM/DoNInDDpAjFeaqCWrYdwENX7ZVM3we8f4swI71tL28N7sg588aB//A78AA"; -describe("migration", () => { - const app = new App({ - fragment: heartMigrationFragment, - }); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); - - it("Loads the project from the URL", async () => { +test.describe("migration", () => { + test("Loads the project from the URL", async ({ app }) => { + await app.goto({ fragment: heartMigrationFragment }); await app.findProjectName("Hearts"); await app.findVisibleEditorContents( - "from microbit import *\ndisplay.show(Image.HEART)" + "from microbit import *display.show(Image.HEART)" ); // Regression test: Check that we can switch to a different migration in the same session. // Previously we ignored the migration because we already had content in session storage. - app.setOptions({ + await app.goto({ fragment: sunlightSensorMigrationFragment, }); - await app.gotoOptionsUrl(); + await app.page.reload(); + // wait for page to load + await app.saveButton.waitFor(); await app.findVisibleEditorContents("display.read_light_level"); }); }); diff --git a/src/e2e/multiple-files.spec.ts b/src/e2e/multiple-files.spec.ts deleted file mode 100644 index 884a11c43..000000000 --- a/src/e2e/multiple-files.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { expect } from "@playwright/test"; -import { LoadDialogType } from "./app-playwright.js"; -import { test } from "./app-test-fixtures.js"; - -test.describe("multiple-files", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("Copes with hex with no Python files", async ({ app }) => { - // Probably best for this to be an error or else we - // need to cope with no Python at all to display. - await app.loadFiles("src/micropython/main/microbit-micropython-v2.hex"); - await app.findAlertText( - "Cannot load file", - "No appended code found in the hex file" - ); - }); - - test("Add a new file", async ({ app }) => { - await app.createNewFile("test"); - - await app.findVisibleEditorContents(/Your new file/); - await app.findProjectFiles(["main.py", "test.py"]); - }); - - test("Prevents deleting main.py", async ({ app }) => { - expect(await app.isDeleteFileOptionDisabled("main.py")).toEqual(true); - }); - - test("Copes with non-main file being updated", async ({ app }) => { - await app.loadFiles("testData/usermodule.py", { - acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, - }); - await app.switchToEditing("usermodule.py"); - await app.findVisibleEditorContents(/b_works/); - - await app.loadFiles("testData/updated/usermodule.py", { - acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, - }); - - await app.findVisibleEditorContents(/c_works/); - }); - - test("Shows warning for third-party module", async ({ app }) => { - await app.loadFiles("testData/module.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - await app.switchToEditing("module.py"); - await app.findThirdPartyModuleWarning("a", "1.0.0"); - - await app.toggleSettingThirdPartyModuleEditing(); - try { - await app.findVisibleEditorContents(/a_works/); - } finally { - await app.toggleSettingThirdPartyModuleEditing(); - } - - await app.loadFiles("testData/updated/module.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - await app.findThirdPartyModuleWarning("a", "1.1.0"); - }); - - test("Copes with currently open file being deleted", async ({ app }) => { - await app.loadFiles("testData/module.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - await app.switchToEditing("module.py"); - - await app.deleteFile("module.py"); - - await app.findVisibleEditorContents(/Hello/); - }); - - test("Muddles through if given non-UTF-8 main.py", async ({ app }) => { - // We could start detect this on open but not sure it's worth it introducting the error cases. - // If we need to recreate the hex then just fill the file with 0xff. - await app.loadFiles("testData/invalid-utf-8.hex"); - - await app.findVisibleEditorContents( - /^����������������������������������������������������������������������������������������������������$/ - ); - }); -}); diff --git a/src/e2e/multiple-files.test.ts b/src/e2e/multiple-files.test.ts index 1b9ce925a..884a11c43 100644 --- a/src/e2e/multiple-files.test.ts +++ b/src/e2e/multiple-files.test.ts @@ -3,37 +3,37 @@ * * SPDX-License-Identifier: MIT */ -import { App, LoadDialogType } from "./app"; +import { expect } from "@playwright/test"; +import { LoadDialogType } from "./app-playwright.js"; +import { test } from "./app-test-fixtures.js"; -describe("multiple-files", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("multiple-files", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("Copes with hex with no Python files", async () => { + test("Copes with hex with no Python files", async ({ app }) => { // Probably best for this to be an error or else we // need to cope with no Python at all to display. await app.loadFiles("src/micropython/main/microbit-micropython-v2.hex"); - await app.findAlertText( "Cannot load file", "No appended code found in the hex file" ); }); - it("Add a new file", async () => { + test("Add a new file", async ({ app }) => { await app.createNewFile("test"); await app.findVisibleEditorContents(/Your new file/); await app.findProjectFiles(["main.py", "test.py"]); }); - it("Prevents deleting main.py", async () => { - expect(await app.canDeleteFile("main.py")).toEqual(false); + test("Prevents deleting main.py", async ({ app }) => { + expect(await app.isDeleteFileOptionDisabled("main.py")).toEqual(true); }); - it("Copes with non-main file being updated", async () => { + test("Copes with non-main file being updated", async ({ app }) => { await app.loadFiles("testData/usermodule.py", { acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, }); @@ -47,7 +47,7 @@ describe("multiple-files", () => { await app.findVisibleEditorContents(/c_works/); }); - it("Shows warning for third-party module", async () => { + test("Shows warning for third-party module", async ({ app }) => { await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); @@ -67,7 +67,7 @@ describe("multiple-files", () => { await app.findThirdPartyModuleWarning("a", "1.1.0"); }); - it("Copes with currently open file being deleted", async () => { + test("Copes with currently open file being deleted", async ({ app }) => { await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); @@ -78,7 +78,7 @@ describe("multiple-files", () => { await app.findVisibleEditorContents(/Hello/); }); - it("Muddles through if given non-UTF-8 main.py", async () => { + test("Muddles through if given non-UTF-8 main.py", async ({ app }) => { // We could start detect this on open but not sure it's worth it introducting the error cases. // If we need to recreate the hex then just fill the file with 0xff. await app.loadFiles("testData/invalid-utf-8.hex"); diff --git a/src/e2e/open.spec.ts b/src/e2e/open.spec.ts deleted file mode 100644 index 0c206b3ce..000000000 --- a/src/e2e/open.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { expect } from "@playwright/test"; -import { LoadDialogType } from "./app-playwright.js"; -import { test } from "./app-test-fixtures.js"; - -test.describe("open", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("Shows an alert when loading a MakeCode hex", async ({ app }) => { - await app.loadFiles("testData/makecode.hex"); - - await app.findAlertText( - "Cannot load file", - "This hex file cannot be loaded in the Python Editor. The Python Editor cannot open hex files created with Microsoft MakeCode." - ); - }); - - test("Loads a Python file", async ({ app }) => { - await app.loadFiles("testData/samplefile.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - - await app.findAlertText("Updated file main.py"); - await app.findProjectName("Untitled project"); - }); - - test("Correctly handles a hex that's actually Python", async ({ app }) => { - await app.loadFiles("testData/not-a-hex.hex", { - acceptDialog: LoadDialogType.NONE, - }); - - await app.findAlertText( - "Cannot load file", - // Would be great to have custom messages here but needs error codes - // pushing into microbit-fs. - "Malformed .hex file, could not parse any registers" - ); - }); - - test("Loads a v1.0.1 hex file", async ({ app }) => { - await app.loadFiles("testData/1.0.1.hex"); - - await app.findVisibleEditorContents(/PASS1/); - await app.findProjectName("1.0.1"); - }); - - test("Loads a v0.9 hex file", async ({ app }) => { - await app.loadFiles("testData/0.9.hex"); - - await app.findVisibleEditorContents(/PASS2/); - await app.findProjectName("0.9"); - }); - - test("Loads via drag and drop", async ({ app }) => { - await app.dropFile("testData/1.0.1.hex"); - - await app.findProjectName("1.0.1"); - // await app.findVisibleEditorContents(/PASS1/); - }); - - test("Correctly handles an mpy file", async ({ app }) => { - await app.loadFiles("testData/samplempyfile.mpy", { - acceptDialog: LoadDialogType.NONE, - }); - - await app.findAlertText( - "Cannot load file", - "This version of the Python Editor doesn't currently support adding .mpy files." - ); - }); - - test("Correctly handles a file with an invalid extension", async ({ - app, - }) => { - await app.loadFiles("testData/sampletxtfile.txt", { - acceptDialog: LoadDialogType.CONFIRM, - }); - - expect(await app.isEditFileOptionDisabled("sampletxtfile.txt")).toEqual( - true - ); - }); - - test("Correctly imports modules with the 'magic comment' in the filesystem.", async ({ - app, - }) => { - await app.loadFiles("testData/module.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - - await app.findAlertText("Added file module.py"); - - await app.loadFiles("testData/module.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - await app.findAlertText("Updated file module.py"); - }); - - test("Warns before load if you have changes", async ({ app }) => { - await app.typeInEditor("# Different text"); - await app.loadFiles("testData/1.0.1.hex", { - acceptDialog: LoadDialogType.REPLACE, - }); - await app.findVisibleEditorContents(/PASS1/); - await app.findProjectName("1.0.1"); - }); - - test("No warn before load if you save hex", async ({ app }) => { - await app.setProjectName("Avoid dialog"); - await app.typeInEditor("# Different text"); - await app.save(); - await app.closeDialog("Project saved"); - - // No dialog accepted - await app.loadFiles("testData/1.0.1.hex"); - await app.findVisibleEditorContents(/PASS1/); - }); - - test("No warn before load if you save main file", async ({ app }) => { - await app.setProjectName("Avoid dialog"); - await app.typeInEditor("# Different text"); - await app.saveMain(); - - // No dialog accepted - await app.loadFiles("testData/1.0.1.hex"); - await app.findVisibleEditorContents(/PASS1/); - }); - - test("Warn before load if you save main file only and you have others", async ({ - app, - }) => { - await app.setProjectName("Avoid dialog"); - await app.typeInEditor("# Different text"); - await app.createNewFile("another"); - await app.saveMain(); - await app.closeDialog("Warning: Only main.py downloaded"); - - await app.loadFiles("testData/1.0.1.hex", { - acceptDialog: LoadDialogType.REPLACE, - }); - await app.findVisibleEditorContents(/PASS1/); - }); -}); diff --git a/src/e2e/open.test.ts b/src/e2e/open.test.ts index bc7487970..0c206b3ce 100644 --- a/src/e2e/open.test.ts +++ b/src/e2e/open.test.ts @@ -3,15 +3,16 @@ * * SPDX-License-Identifier: MIT */ -import { App, LoadDialogType } from "./app"; +import { expect } from "@playwright/test"; +import { LoadDialogType } from "./app-playwright.js"; +import { test } from "./app-test-fixtures.js"; -describe("open", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("open", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("Shows an alert when loading a MakeCode hex", async () => { + test("Shows an alert when loading a MakeCode hex", async ({ app }) => { await app.loadFiles("testData/makecode.hex"); await app.findAlertText( @@ -20,7 +21,7 @@ describe("open", () => { ); }); - it("Loads a Python file", async () => { + test("Loads a Python file", async ({ app }) => { await app.loadFiles("testData/samplefile.py", { acceptDialog: LoadDialogType.CONFIRM, }); @@ -29,7 +30,7 @@ describe("open", () => { await app.findProjectName("Untitled project"); }); - it("Correctly handles a hex that's actually Python", async () => { + test("Correctly handles a hex that's actually Python", async ({ app }) => { await app.loadFiles("testData/not-a-hex.hex", { acceptDialog: LoadDialogType.NONE, }); @@ -42,28 +43,28 @@ describe("open", () => { ); }); - it("Loads a v1.0.1 hex file", async () => { + test("Loads a v1.0.1 hex file", async ({ app }) => { await app.loadFiles("testData/1.0.1.hex"); await app.findVisibleEditorContents(/PASS1/); await app.findProjectName("1.0.1"); }); - it("Loads a v0.9 hex file", async () => { + test("Loads a v0.9 hex file", async ({ app }) => { await app.loadFiles("testData/0.9.hex"); await app.findVisibleEditorContents(/PASS2/); await app.findProjectName("0.9"); }); - it("Loads via drag and drop", async () => { + test("Loads via drag and drop", async ({ app }) => { await app.dropFile("testData/1.0.1.hex"); await app.findProjectName("1.0.1"); - await app.findVisibleEditorContents(/PASS1/); + // await app.findVisibleEditorContents(/PASS1/); }); - it("Correctly handles an mpy file", async () => { + test("Correctly handles an mpy file", async ({ app }) => { await app.loadFiles("testData/samplempyfile.mpy", { acceptDialog: LoadDialogType.NONE, }); @@ -74,15 +75,21 @@ describe("open", () => { ); }); - it("Correctly handles a file with an invalid extension", async () => { + test("Correctly handles a file with an invalid extension", async ({ + app, + }) => { await app.loadFiles("testData/sampletxtfile.txt", { acceptDialog: LoadDialogType.CONFIRM, }); - expect(await app.canSwitchToEditing("sampletxtfile.txt")).toEqual(false); + expect(await app.isEditFileOptionDisabled("sampletxtfile.txt")).toEqual( + true + ); }); - it("Correctly imports modules with the 'magic comment' in the filesystem.", async () => { + test("Correctly imports modules with the 'magic comment' in the filesystem.", async ({ + app, + }) => { await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); @@ -95,7 +102,7 @@ describe("open", () => { await app.findAlertText("Updated file module.py"); }); - it("Warns before load if you have changes", async () => { + test("Warns before load if you have changes", async ({ app }) => { await app.typeInEditor("# Different text"); await app.loadFiles("testData/1.0.1.hex", { acceptDialog: LoadDialogType.REPLACE, @@ -104,7 +111,7 @@ describe("open", () => { await app.findProjectName("1.0.1"); }); - it("No warn before load if you save hex", async () => { + test("No warn before load if you save hex", async ({ app }) => { await app.setProjectName("Avoid dialog"); await app.typeInEditor("# Different text"); await app.save(); @@ -115,7 +122,7 @@ describe("open", () => { await app.findVisibleEditorContents(/PASS1/); }); - it("No warn before load if you save main file", async () => { + test("No warn before load if you save main file", async ({ app }) => { await app.setProjectName("Avoid dialog"); await app.typeInEditor("# Different text"); await app.saveMain(); @@ -125,7 +132,9 @@ describe("open", () => { await app.findVisibleEditorContents(/PASS1/); }); - it("Warn before load if you save main file only and you have others", async () => { + test("Warn before load if you save main file only and you have others", async ({ + app, + }) => { await app.setProjectName("Avoid dialog"); await app.typeInEditor("# Different text"); await app.createNewFile("another"); diff --git a/src/e2e/reset.spec.ts b/src/e2e/reset.spec.ts deleted file mode 100644 index 5158a6090..000000000 --- a/src/e2e/reset.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -test.describe("reset", () => { - test("sets language via URL", async ({ app }) => { - await app.goto(); - await app.setProjectName("My project"); - await app.selectAllInEditor(); - await app.typeInEditor("# Not the default starter code"); - await app.createNewFile("testing"); - - await app.resetProject(); - - // Everything's back to normal. - await app.findProjectName("Untitled project"); - await app.findVisibleEditorContents("from microbit import"); - await app.findProjectFiles(["main.py"]); - }); -}); diff --git a/src/e2e/reset.test.ts b/src/e2e/reset.test.ts index d497b6fb5..5158a6090 100644 --- a/src/e2e/reset.test.ts +++ b/src/e2e/reset.test.ts @@ -3,15 +3,11 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; -describe("reset", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); - - it("resets the project", async () => { +test.describe("reset", () => { + test("sets language via URL", async ({ app }) => { + await app.goto(); await app.setProjectName("My project"); await app.selectAllInEditor(); await app.typeInEditor("# Not the default starter code"); diff --git a/src/e2e/save.spec.ts b/src/e2e/save.spec.ts deleted file mode 100644 index 2112beae1..000000000 --- a/src/e2e/save.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { expect } from "@playwright/test"; -import fs from "fs"; -import { LoadDialogType } from "./app-playwright.js"; -import { test } from "./app-test-fixtures.js"; - -test.describe("save", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("Download - save the default HEX asd", async ({ app }) => { - await app.setProjectName("idiosyncratic ruminant"); - const download = await app.save(); - if (!download) { - throw new Error("Invalid download"); - } - const filename = download.suggestedFilename(); - expect(filename).toEqual("idiosyncratic ruminant.hex"); - - const path = await download.path(); - if (!path) { - throw new Error("Invalid path"); - } - const contents = await fs.promises.readFile(path, { encoding: "ascii" }); - expect(contents).toMatch(/^:020000040000FA/); - }); - - test("Shows an error when trying to save a hex file if the Python code is too large", async ({ - app, - }) => { - // Set the project name to avoid calling the edit project name input dialog. - await app.setProjectName("not default name"); - await app.loadFiles("testData/too-large.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - await app.findVisibleEditorContents(/# Filler/); - await app.save({ waitForDownload: false }); - - await app.findAlertText( - "Failed to build the hex file", - "There is no storage space left." - ); - }); - - test("Shows the name your project dialog if the project name is the default", async ({ - app, - }) => { - await app.save({ waitForDownload: false }); - await app.confirmInputDialog("Name your project"); - }); - - test("Shows the post-save dialog after hex save", async ({ app }) => { - await app.setProjectName("not default name"); - await app.save(); - await app.confirmInputDialog("Project saved"); - }); - - test("Shows the multiple files dialog after main.py save if there are multiple files in the project", async ({ - app, - }) => { - await app.setProjectName("not default name"); - await app.loadFiles("testData/module.py", { - acceptDialog: LoadDialogType.CONFIRM, - }); - await app.saveMain(); - await app.confirmInputDialog("Warning: Only main.py downloaded"); - }); -}); diff --git a/src/e2e/save.test.ts b/src/e2e/save.test.ts index ca0aebc4b..2112beae1 100644 --- a/src/e2e/save.test.ts +++ b/src/e2e/save.test.ts @@ -3,30 +3,43 @@ * * SPDX-License-Identifier: MIT */ -import { App, LoadDialogType } from "./app"; +import { expect } from "@playwright/test"; +import fs from "fs"; +import { LoadDialogType } from "./app-playwright.js"; +import { test } from "./app-test-fixtures.js"; -describe("save", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("save", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("Download - save the default HEX asd", async () => { + test("Download - save the default HEX asd", async ({ app }) => { await app.setProjectName("idiosyncratic ruminant"); - const download = await app.waitForSave(); + const download = await app.save(); + if (!download) { + throw new Error("Invalid download"); + } + const filename = download.suggestedFilename(); + expect(filename).toEqual("idiosyncratic ruminant.hex"); - expect(download.filename).toEqual("idiosyncratic ruminant.hex"); - expect(download.data.toString("ascii")).toMatch(/^:020000040000FA/); + const path = await download.path(); + if (!path) { + throw new Error("Invalid path"); + } + const contents = await fs.promises.readFile(path, { encoding: "ascii" }); + expect(contents).toMatch(/^:020000040000FA/); }); - it("Shows an error when trying to save a hex file if the Python code is too large", async () => { + test("Shows an error when trying to save a hex file if the Python code is too large", async ({ + app, + }) => { // Set the project name to avoid calling the edit project name input dialog. await app.setProjectName("not default name"); await app.loadFiles("testData/too-large.py", { acceptDialog: LoadDialogType.CONFIRM, }); await app.findVisibleEditorContents(/# Filler/); - await app.save(); + await app.save({ waitForDownload: false }); await app.findAlertText( "Failed to build the hex file", @@ -34,23 +47,27 @@ describe("save", () => { ); }); - it("Shows the name your project dialog if the project name is the default", async () => { - await app.save(); + test("Shows the name your project dialog if the project name is the default", async ({ + app, + }) => { + await app.save({ waitForDownload: false }); await app.confirmInputDialog("Name your project"); }); - it("Shows the post-save dialog after hex save", async () => { + test("Shows the post-save dialog after hex save", async ({ app }) => { await app.setProjectName("not default name"); await app.save(); - await app.confirmGenericDialog("Project saved"); + await app.confirmInputDialog("Project saved"); }); - it("Shows the multiple files dialog after main.py save if there are multiple files in the project", async () => { + test("Shows the multiple files dialog after main.py save if there are multiple files in the project", async ({ + app, + }) => { await app.setProjectName("not default name"); await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); await app.saveMain(); - await app.confirmGenericDialog("Warning: Only main.py downloaded"); + await app.confirmInputDialog("Warning: Only main.py downloaded"); }); }); diff --git a/src/e2e/settings.spec.ts b/src/e2e/settings.spec.ts deleted file mode 100644 index 412221d5d..000000000 --- a/src/e2e/settings.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -test.describe("settings", () => { - test("sets language via URL", async ({ app }) => { - await app.goto({ language: "fr" }); - // French via the URL - await app.findProjectName("Projet sans titre"); - - await app.switchLanguage("en"); - await app.page.reload(); - // French URL ignored as we've made an explicit language choice. - await app.findProjectName("Untitled project"); - }); - - test("switches language", async ({ app }) => { - await app.goto(); - await app.switchLanguage("fr"); - await app.findProjectName("Projet sans titre"); - await app.switchLanguage("en"); - await app.findProjectName("Untitled project"); - }); -}); diff --git a/src/e2e/settings.test.ts b/src/e2e/settings.test.ts index 06476297b..412221d5d 100644 --- a/src/e2e/settings.test.ts +++ b/src/e2e/settings.test.ts @@ -3,33 +3,22 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; -describe("settings", () => { - const app = new App(); - beforeEach(() => { - app.setOptions({}); - return app.reset(); - }); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); - - it("sets language via URL", async () => { - app.setOptions({ - language: "fr", - }); - await app.reset(); +test.describe("settings", () => { + test("sets language via URL", async ({ app }) => { + await app.goto({ language: "fr" }); // French via the URL await app.findProjectName("Projet sans titre"); await app.switchLanguage("en"); - await app.reset(); + await app.page.reload(); // French URL ignored as we've made an explicit language choice. await app.findProjectName("Untitled project"); }); - it("switches language", async () => { - // NOTE: the app methods generally won't still work after changing language. + test("switches language", async ({ app }) => { + await app.goto(); await app.switchLanguage("fr"); await app.findProjectName("Projet sans titre"); await app.switchLanguage("en"); diff --git a/src/e2e/simulator.spec.ts b/src/e2e/simulator.spec.ts deleted file mode 100644 index ab5bb29b5..000000000 --- a/src/e2e/simulator.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * (c) 2021, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { test } from "./app-test-fixtures.js"; - -const basicTest = "from microbit import *\ndisplay.show(Image.NO)"; - -const buttonTest = - "from microbit import *\nwhile True:\nif button_a.was_pressed():\ndisplay.show(Image.NO)"; - -const gestureTest = - "from microbit import *\nwhile True:\nif accelerometer.was_gesture('freefall'):\ndisplay.show(Image.NO)"; - -const sliderTest = - "from microbit import *\nwhile True:\nif temperature() == -5:\ndisplay.show(Image.NO)"; - -test.describe("simulator", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - - test("responds to a sent gesture", async ({ app }) => { - // Enum sensor change via select and button. - await app.selectAllInEditor(); - await app.typeInEditor(gestureTest); - await app.runSimulator(); - await app.simulatorSelectGesture("freefall"); - await app.simulatorSendGesture(); - await app.simulatorConfirmResponse(); - }); - - test("responds to a range sensor change", async ({ app }) => { - // Range sensor change via slider. - await app.selectAllInEditor(); - await app.typeInEditor(sliderTest); - await app.runSimulator(); - await app.simulatorSetRangeSlider("Temperature", "min"); - await app.simulatorConfirmResponse(); - }); - - test("responds to a button press", async ({ app }) => { - // Range sensor change via button. - await app.selectAllInEditor(); - await app.typeInEditor(buttonTest); - await app.runSimulator(); - await app.simulatorInputPressHold("Press button A", 500); - await app.simulatorConfirmResponse(); - }); - test("stops when the code changes", async ({ app }) => { - await app.selectAllInEditor(); - await app.typeInEditor(basicTest); - await app.runSimulator(); - await app.simulatorConfirmResponse(); - - await app.typeInEditor("A change!"); - - await app.findStoppedSimulator(); - }); -}); diff --git a/src/e2e/simulator.test.ts b/src/e2e/simulator.test.ts index bb038db6b..ab5bb29b5 100644 --- a/src/e2e/simulator.test.ts +++ b/src/e2e/simulator.test.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: MIT */ -import { App } from "./app"; +import { test } from "./app-test-fixtures.js"; const basicTest = "from microbit import *\ndisplay.show(Image.NO)"; @@ -16,13 +16,12 @@ const gestureTest = const sliderTest = "from microbit import *\nwhile True:\nif temperature() == -5:\ndisplay.show(Image.NO)"; -describe("simulator", () => { - const app = new App(); - beforeEach(app.reset.bind(app)); - afterEach(app.screenshot.bind(app)); - afterAll(app.dispose.bind(app)); +test.describe("simulator", () => { + test.beforeEach(async ({ app }) => { + await app.goto(); + }); - it("responds to a sent gesture", async () => { + test("responds to a sent gesture", async ({ app }) => { // Enum sensor change via select and button. await app.selectAllInEditor(); await app.typeInEditor(gestureTest); @@ -32,7 +31,7 @@ describe("simulator", () => { await app.simulatorConfirmResponse(); }); - it("responds to a range sensor change", async () => { + test("responds to a range sensor change", async ({ app }) => { // Range sensor change via slider. await app.selectAllInEditor(); await app.typeInEditor(sliderTest); @@ -41,7 +40,7 @@ describe("simulator", () => { await app.simulatorConfirmResponse(); }); - it("responds to a button press", async () => { + test("responds to a button press", async ({ app }) => { // Range sensor change via button. await app.selectAllInEditor(); await app.typeInEditor(buttonTest); @@ -49,7 +48,7 @@ describe("simulator", () => { await app.simulatorInputPressHold("Press button A", 500); await app.simulatorConfirmResponse(); }); - it("stops when the code changes", async () => { + test("stops when the code changes", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor(basicTest); await app.runSimulator(); From 83f8bf6619eb8ad43aaf22faf53f834fc2f783cd Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 16:51:28 +0000 Subject: [PATCH 11/38] Remove playwright test match for *.spec.ts --- playwright.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index f8cc8a17c..5b8972be6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -13,8 +13,6 @@ export default defineConfig({ use: { trace: "on-first-retry", }, - // ignore *.test.ts for now - testMatch: "*.spec.ts", /* Configure projects for major browsers */ projects: [ From bf327655a6a872436d7188a4a3d4eeab8a1e8f2c Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 16:51:41 +0000 Subject: [PATCH 12/38] Update README.md --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9742a32f1..5994c9d56 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ These are excluded from the normal test run. The tests expect the app to already be running on http://localhost:3000, for example via `npm start`. -We use [Puppeteer](https://pptr.dev/) and the helpers provided by [Testing Library](https://testing-library.com/docs/pptr-testing-library/intro/). +We use [Playwright](https://playwright.dev/). The CI tests run these end-to-end tests against a production build. diff --git a/package.json b/package.json index d6f8eff56..ad2c5524c 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "serve": "npx serve --no-clipboard -l 3000 -- build/", "start": "vite dev", "test:e2e:headless": "cross-env E2E_HEADLESS=1 vitest --mode e2e", - "test:e2e": "cross-env E2E_HEADLESS=0 vitest --mode e2e", + "test:e2e": "npx playwright test --ui", "test": "vitest", "theme:watch": "chakra-cli tokens src/deployment/default/theme.ts --watch", "theme": "chakra-cli tokens src/deployment/default/theme.ts", From db74dbcfd383a0d949afe6f86c9bf86ad38e13b0 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 17:04:19 +0000 Subject: [PATCH 13/38] Add navigation to app into fixture --- e2e/example.spec.ts | 18 ------------------ src/e2e/accessibility.test.ts | 4 ---- src/e2e/app-test-fixtures.ts | 1 + src/e2e/autocomplete.test.ts | 4 ---- src/e2e/connect.test.ts | 4 ---- src/e2e/documentation.test.ts | 4 ---- src/e2e/edits.test.ts | 4 ---- src/e2e/migration.test.ts | 1 + src/e2e/multiple-files.test.ts | 4 ---- src/e2e/open.test.ts | 4 ---- src/e2e/reset.test.ts | 1 - src/e2e/save.test.ts | 4 ---- src/e2e/settings.test.ts | 1 - src/e2e/simulator.test.ts | 4 ---- 14 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 e2e/example.spec.ts diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts deleted file mode 100644 index 54a906a4e..000000000 --- a/e2e/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/src/e2e/accessibility.test.ts b/src/e2e/accessibility.test.ts index 2e4257405..0f2c01891 100644 --- a/src/e2e/accessibility.test.ts +++ b/src/e2e/accessibility.test.ts @@ -6,10 +6,6 @@ import { test } from "./app-test-fixtures.js"; test.describe("accessibility", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("focuses the correct element on tabbing after load", async ({ app }) => { await app.assertFocusOnLoad(); }); diff --git a/src/e2e/app-test-fixtures.ts b/src/e2e/app-test-fixtures.ts index b48071734..e17ba7d91 100644 --- a/src/e2e/app-test-fixtures.ts +++ b/src/e2e/app-test-fixtures.ts @@ -29,6 +29,7 @@ export const test = base.extend({ url: app.baseUrl, }, ]); + await app.goto(); await use(app); }, }); diff --git a/src/e2e/autocomplete.test.ts b/src/e2e/autocomplete.test.ts index 1630757fc..7fd062c43 100644 --- a/src/e2e/autocomplete.test.ts +++ b/src/e2e/autocomplete.test.ts @@ -9,10 +9,6 @@ const showFullSignature = "show(image, delay=400, wait=True, loop=False, clear=False)"; test.describe("autocomplete", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("shows autocomplete as you type", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.s"); diff --git a/src/e2e/connect.test.ts b/src/e2e/connect.test.ts index 9dd704718..d407fde00 100644 --- a/src/e2e/connect.test.ts +++ b/src/e2e/connect.test.ts @@ -11,10 +11,6 @@ SyntaxError: invalid syntax `; // Needs trailing newline! test.describe("connect", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("shows serial when connected", async ({ app }) => { // Connect and disconnect wait for serial to be shown/hidden await app.connect(); diff --git a/src/e2e/documentation.test.ts b/src/e2e/documentation.test.ts index 8b4ac501f..e318b2c0f 100644 --- a/src/e2e/documentation.test.ts +++ b/src/e2e/documentation.test.ts @@ -6,10 +6,6 @@ import { test } from "./app-test-fixtures.js"; test.describe("documentation", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("API toolkit navigation", async ({ app }) => { await app.switchTab("API"); await app.findDocumentationTopLevelHeading( diff --git a/src/e2e/edits.test.ts b/src/e2e/edits.test.ts index efa202a83..e6fec3fb8 100644 --- a/src/e2e/edits.test.ts +++ b/src/e2e/edits.test.ts @@ -6,10 +6,6 @@ import { test } from "./app-test-fixtures.js"; test.describe("edits", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("doesn't prompt on close if no edits made", async ({ app }) => { await app.closePageCheckDialog(false); }); diff --git a/src/e2e/migration.test.ts b/src/e2e/migration.test.ts index e0acccb20..a81e23c9f 100644 --- a/src/e2e/migration.test.ts +++ b/src/e2e/migration.test.ts @@ -14,6 +14,7 @@ const sunlightSensorMigrationFragment = test.describe("migration", () => { test("Loads the project from the URL", async ({ app }) => { await app.goto({ fragment: heartMigrationFragment }); + await app.page.reload(); await app.findProjectName("Hearts"); await app.findVisibleEditorContents( "from microbit import *display.show(Image.HEART)" diff --git a/src/e2e/multiple-files.test.ts b/src/e2e/multiple-files.test.ts index 884a11c43..c99ca9cef 100644 --- a/src/e2e/multiple-files.test.ts +++ b/src/e2e/multiple-files.test.ts @@ -8,10 +8,6 @@ import { LoadDialogType } from "./app-playwright.js"; import { test } from "./app-test-fixtures.js"; test.describe("multiple-files", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("Copes with hex with no Python files", async ({ app }) => { // Probably best for this to be an error or else we // need to cope with no Python at all to display. diff --git a/src/e2e/open.test.ts b/src/e2e/open.test.ts index 0c206b3ce..b3259f2a4 100644 --- a/src/e2e/open.test.ts +++ b/src/e2e/open.test.ts @@ -8,10 +8,6 @@ import { LoadDialogType } from "./app-playwright.js"; import { test } from "./app-test-fixtures.js"; test.describe("open", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("Shows an alert when loading a MakeCode hex", async ({ app }) => { await app.loadFiles("testData/makecode.hex"); diff --git a/src/e2e/reset.test.ts b/src/e2e/reset.test.ts index 5158a6090..66c99048c 100644 --- a/src/e2e/reset.test.ts +++ b/src/e2e/reset.test.ts @@ -7,7 +7,6 @@ import { test } from "./app-test-fixtures.js"; test.describe("reset", () => { test("sets language via URL", async ({ app }) => { - await app.goto(); await app.setProjectName("My project"); await app.selectAllInEditor(); await app.typeInEditor("# Not the default starter code"); diff --git a/src/e2e/save.test.ts b/src/e2e/save.test.ts index 2112beae1..01902b1da 100644 --- a/src/e2e/save.test.ts +++ b/src/e2e/save.test.ts @@ -9,10 +9,6 @@ import { LoadDialogType } from "./app-playwright.js"; import { test } from "./app-test-fixtures.js"; test.describe("save", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("Download - save the default HEX asd", async ({ app }) => { await app.setProjectName("idiosyncratic ruminant"); const download = await app.save(); diff --git a/src/e2e/settings.test.ts b/src/e2e/settings.test.ts index 412221d5d..71853d977 100644 --- a/src/e2e/settings.test.ts +++ b/src/e2e/settings.test.ts @@ -18,7 +18,6 @@ test.describe("settings", () => { }); test("switches language", async ({ app }) => { - await app.goto(); await app.switchLanguage("fr"); await app.findProjectName("Projet sans titre"); await app.switchLanguage("en"); diff --git a/src/e2e/simulator.test.ts b/src/e2e/simulator.test.ts index ab5bb29b5..62839be62 100644 --- a/src/e2e/simulator.test.ts +++ b/src/e2e/simulator.test.ts @@ -17,10 +17,6 @@ const sliderTest = "from microbit import *\nwhile True:\nif temperature() == -5:\ndisplay.show(Image.NO)"; test.describe("simulator", () => { - test.beforeEach(async ({ app }) => { - await app.goto(); - }); - test("responds to a sent gesture", async ({ app }) => { // Enum sensor change via select and button. await app.selectAllInEditor(); From 9a58c4e004455576916436f32fe08848851a5a16 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 17:04:33 +0000 Subject: [PATCH 14/38] Update package-lock.json --- package-lock.json | 1358 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 1129 insertions(+), 229 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8694c232..a49a1721a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1793,9 +1793,9 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.14.0.tgz", - "integrity": "sha512-Kx9BCSOLKmqNXEvmViuzsBQJ2VEa/wWwOATNpixOa+suttTV3rDnAUtAIt5ObAUFjXvZakWfFfF/EbxELnGLzQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.15.0.tgz", + "integrity": "sha512-G2Zm0mXznxz97JhaaOdoEG2cVupn4JjPaS4AcNvZzhOsnnG9YVN68VzfoUw6dYTsIxT6a/cmoFEN47KAWhXaOg==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -1851,9 +1851,9 @@ "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" }, "node_modules/@codemirror/view": { - "version": "6.25.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.25.1.tgz", - "integrity": "sha512-2LXLxsQnHDdfGzDvjzAwZh2ZviNJm7im6tGpa0IONIDnFd8RZ80D2SNi8PDi6YjKcMoMRK20v6OmKIdsrwsyoQ==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.0.tgz", + "integrity": "sha512-nSSmzONpqsNzshPOxiKhK203R6BvABepugAe34QfQDbNDslyjkqBuKgrK5ZBvqNXpfxz5iLrlGTmEfhbQyH46A==", "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", @@ -1998,6 +1998,69 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", @@ -2014,6 +2077,294 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2061,6 +2412,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2076,6 +2437,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -2205,6 +2578,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2405,9 +2800,9 @@ } }, "node_modules/@lezer/python": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.11.tgz", - "integrity": "sha512-C3QeLCcdAKJDUOsYjfFP6a1wdn8jhUNX200bgFm8TpKH1eM2PlgYQS5ugw6E38qGeEx7CP21I1Q52SoybXt0OQ==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.12.tgz", + "integrity": "sha512-jDfUgOIDulv94R89dtYBfmIpCHiKn6RkeeVT7RQmbaKehJEMp30Bj5fHdAsgA2p8Gqjj+mbHVR+jyxUzSUNaOg==", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -2504,20 +2899,6 @@ "fsevents": "2.3.2" } }, - "node_modules/@playwright/test/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/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2549,6 +2930,30 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", @@ -2561,6 +2966,126 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sanity/block-content-to-hyperscript": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sanity/block-content-to-hyperscript/-/block-content-to-hyperscript-3.0.0.tgz", @@ -3252,9 +3777,9 @@ "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==" }, "node_modules/@types/node": { - "version": "20.11.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", - "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "version": "20.11.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz", + "integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==", "dependencies": { "undici-types": "~5.26.4" } @@ -3278,9 +3803,9 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { - "version": "18.2.65", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.65.tgz", - "integrity": "sha512-98TsY0aW4jqx/3RqsUXwMDZSWR1Z4CUlJNue8ueS2/wcxZOsz4xmW1X8ieaWVRHcmmQM3R8xVA4XWB3dJnWwDQ==", + "version": "18.2.66", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz", + "integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3288,9 +3813,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", - "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", "dependencies": { "@types/react": "*" } @@ -3545,15 +4070,6 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3566,21 +4082,6 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -3703,13 +4204,13 @@ } }, "node_modules/@vitest/expect": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", - "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz", + "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==", "dev": true, "dependencies": { - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "chai": "^4.3.10" }, "funding": { @@ -3717,12 +4218,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", - "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz", + "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==", "dev": true, "dependencies": { - "@vitest/utils": "1.3.1", + "@vitest/utils": "1.4.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -3745,10 +4246,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@vitest/snapshot": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", - "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz", + "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -3792,9 +4305,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", - "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz", + "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -3804,9 +4317,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", - "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz", + "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -4225,12 +4738,15 @@ ] }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { @@ -4245,13 +4761,12 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -5078,9 +5593,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.701", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", - "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==", + "version": "1.4.707", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.707.tgz", + "integrity": "sha512-qRq74Mo7ChePOU6GHdfAJ0NREXU8vQTlVlfWz3wNygFay6xrd/fY2J7oGHwrhFeU30OVctGLdTh/FcnokTWpng==", "dev": true }, "node_modules/end-of-stream": { @@ -5435,8 +5950,18 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz", "integrity": "sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==", "dev": true, - "peerDependencies": { - "eslint": ">=7" + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -5451,6 +5976,18 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -5511,6 +6048,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5545,22 +6092,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5597,49 +6128,16 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "p-limit": "^3.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, "node_modules/eslint/node_modules/supports-color": { @@ -5654,18 +6152,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -5888,15 +6374,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -5927,16 +6404,19 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { @@ -6057,9 +6537,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "optional": true, "os": [ @@ -6213,6 +6693,28 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7003,6 +7505,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/jake/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7046,6 +7558,18 @@ "node": ">=8" } }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jake/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7679,15 +8203,18 @@ } }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -7883,15 +8410,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mkdirp-classic": { @@ -8217,30 +8747,33 @@ } }, "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -8376,7 +8909,59 @@ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "dependencies": { - "find-up": "^4.0.0" + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" }, "engines": { "node": ">=8" @@ -8422,19 +9007,6 @@ "node": ">=14" } }, - "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==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/playwright/node_modules/playwright-core": { "version": "1.42.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", @@ -8986,9 +9558,9 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", - "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -9932,9 +10504,9 @@ } }, "node_modules/ufo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", - "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.0.tgz", + "integrity": "sha512-c7SxU8XB0LTO7hALl6CcE1Q92ZrLzr1iE0IVIsUa9SlFfkn2B2p6YLO6dLxOj7qCWY98PB3Q3EZbN6bEu8p7jA==", "dev": true }, "node_modules/unbox-primitive": { @@ -10127,9 +10699,9 @@ } }, "node_modules/vite-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", - "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz", + "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -10162,6 +10734,51 @@ "vite": "^2.6.0 || 3 || 4 || 5" } }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", @@ -10177,6 +10794,276 @@ "node": ">=12" } }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vite/node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", @@ -10214,17 +11101,30 @@ "@esbuild/win32-x64": "0.19.12" } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitest": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", - "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", + "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==", "dev": true, "dependencies": { - "@vitest/expect": "1.3.1", - "@vitest/runner": "1.3.1", - "@vitest/snapshot": "1.3.1", - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", + "@vitest/expect": "1.4.0", + "@vitest/runner": "1.4.0", + "@vitest/snapshot": "1.4.0", + "@vitest/spy": "1.4.0", + "@vitest/utils": "1.4.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -10238,7 +11138,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.3.1", + "vite-node": "1.4.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -10253,8 +11153,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.3.1", - "@vitest/ui": "1.3.1", + "@vitest/browser": "1.4.0", + "@vitest/ui": "1.4.0", "happy-dom": "*", "jsdom": "*" }, @@ -10562,12 +11462,12 @@ } }, "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "engines": { - "node": ">=12.20" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" From abc1a3e11b853d83c30e8f4157a895d4a653bac4 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 17:06:59 +0000 Subject: [PATCH 15/38] Remove test examples from playwright init --- tests-examples/demo-todo-app.spec.ts | 437 --------------------------- 1 file changed, 437 deletions(-) delete mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 2fd6016fe..000000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -]; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} From 9d15e4de2b3dea60aab6bef65d6e93fd742c0c09 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 15 Mar 2024 17:43:46 +0000 Subject: [PATCH 16/38] Refactoring (renaming and tidying) WIP --- src/e2e/app-playwright.ts | 40 ++++++++++++---------------------- src/e2e/autocomplete.test.ts | 4 ++-- src/e2e/documentation.test.ts | 14 +++++------- src/e2e/edits.test.ts | 8 +++---- src/e2e/migration.test.ts | 6 ++--- src/e2e/multiple-files.test.ts | 12 +++++----- src/e2e/open.test.ts | 22 +++++++++---------- src/e2e/reset.test.ts | 4 ++-- src/e2e/save.test.ts | 2 +- src/e2e/settings.test.ts | 8 +++---- 10 files changed, 53 insertions(+), 67 deletions(-) diff --git a/src/e2e/app-playwright.ts b/src/e2e/app-playwright.ts index 00f83cca1..eb4294346 100644 --- a/src/e2e/app-playwright.ts +++ b/src/e2e/app-playwright.ts @@ -177,11 +177,14 @@ export class App { await this.editor.waitFor(); } - // TODO: Rename to expectProjectName - async findProjectName(match: string) { - await expect( - this.page.getByTestId("project-name").getByText(match) - ).toBeVisible(); + async setProjectName(projectName: string): Promise { + await this.page.getByRole("button", { name: "Edit project name" }).click(); + await this.page.getByLabel("Name*").fill(projectName); + await this.page.getByRole("button", { name: "Confirm" }).click(); + } + + async expectProjectName(match: string) { + await expect(this.page.getByTestId("project-name")).toHaveText(match); } async switchLanguage(locale: string) { @@ -191,42 +194,32 @@ export class App { await this.page.getByTestId(locale).click(); } - async setProjectName(projectName: string): Promise { - await this.page.getByRole("button", { name: "Edit project name" }).click(); - await this.page.getByLabel("Name*").fill(projectName); - await this.page.getByRole("button", { name: "Confirm" }).click(); - } - async selectAllInEditor(): Promise { await this.editorTextArea.click(); await this.page.keyboard.press(`${this.modifierKey}+A`); } // TODO: Rename to pasteInEditor - async pasteToolkitCode() { + async pasteInEditor() { // Simulating keyboard press CTRL+V works in Playwright, // but does not work in this case potentially due to // CodeMirror pasting magic const clipboardText: string = await this.page.evaluate( "navigator.clipboard.readText()" ); - await this.editorTextArea.evaluate((el, clipboardText1) => { - const text = clipboardText1; + await this.editorTextArea.evaluate((el, text) => { const clipboardData = new DataTransfer(); clipboardData.setData("text/plain", text); - const clipboardEvent = new ClipboardEvent("paste", { - clipboardData, - }); + const clipboardEvent = new ClipboardEvent("paste", { clipboardData }); el.dispatchEvent(clipboardEvent); }, clipboardText); } async typeInEditor(text: string): Promise { const textWithoutLastChar = text.slice(0, text.length - 1); - const lastChar = text.slice(-1); await this.editorTextArea.fill(textWithoutLastChar); // Last character is typed separately to trigger editor suggestions - await this.page.keyboard.press(lastChar); + await this.page.keyboard.press(text.slice(-1)); } async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { @@ -246,9 +239,8 @@ export class App { await this.page.getByRole("button", { name: "Replace" }).click(); } - // TODO: Rename to expectEditorContentsContain // Use allInnerTexts() for matching text - async findVisibleEditorContents(match: RegExp | string) { + async expectEditorContainText(match: RegExp | string) { // Scroll to the top of code text area await this.editorTextArea.click(); await this.page.mouse.wheel(0, -100000000); @@ -421,11 +413,6 @@ export class App { await this.page.getByRole("heading", { name }).click(); } - // Rename srollToTop - async triggerScroll(_tabName: string) { - await this.page.mouse.wheel(0, -100000000); - } - async toggleCodeActionButton(name: string): Promise { await this.page .getByRole("listitem") @@ -591,6 +578,7 @@ export class App { } // TODO: Rename to expectCompletionOptions + // try toContainText instead for testing! async findCompletionOptions(expected: string[]): Promise { const completions = this.page.getByRole("listbox", { name: "Completions" }); const contents = await completions.innerText(); diff --git a/src/e2e/autocomplete.test.ts b/src/e2e/autocomplete.test.ts index 7fd062c43..81965a7ac 100644 --- a/src/e2e/autocomplete.test.ts +++ b/src/e2e/autocomplete.test.ts @@ -23,7 +23,7 @@ test.describe("autocomplete", () => { // Accepted completion await app.acceptCompletion("show"); - await app.findVisibleEditorContents("display.show()"); + await app.expectEditorContainText("display.show()"); }); test("ranks Image above image=", async ({ app }) => { @@ -71,7 +71,7 @@ test.describe("autocomplete", () => { await app.typeInEditor("from audio import is_pla"); await app.acceptCompletion("is_playing"); - await app.findVisibleEditorContents(/is_playing$/); + await app.expectEditorContainText(/is_playing$/); }); test("signature can navigate to API toolkit content", async ({ app }) => { diff --git a/src/e2e/documentation.test.ts b/src/e2e/documentation.test.ts index e318b2c0f..e90e6c5ed 100644 --- a/src/e2e/documentation.test.ts +++ b/src/e2e/documentation.test.ts @@ -20,11 +20,10 @@ test.describe("documentation", () => { await app.typeInEditor("# Initial document"); await app.switchTab(tab); await app.selectDocumentationSection("Display"); - await app.triggerScroll(tab); await app.toggleCodeActionButton("Images: built-in"); await app.copyCode("Images: built-in"); - await app.pasteToolkitCode(); - await app.findVisibleEditorContents("display.show(Image.HEART)"); + await app.pasteInEditor(); + await app.expectEditorContainText("display.show(Image.HEART)"); }); test("Copy code after dropdown choice and paste in editor", async ({ @@ -35,7 +34,6 @@ test.describe("documentation", () => { await app.typeInEditor("# Initial document"); await app.switchTab(tab); await app.selectDocumentationSection("Display"); - await app.triggerScroll(tab); await app.selectToolkitDropDownOption( "Select image:", "silly" // "Image.SILLY" @@ -43,14 +41,14 @@ test.describe("documentation", () => { await app.toggleCodeActionButton("Images: built-in"); await app.copyCode("Images: built-in"); - await app.pasteToolkitCode(); - await app.findVisibleEditorContents("display.show(Image.SILLY)"); + await app.pasteInEditor(); + await app.expectEditorContainText("display.show(Image.SILLY)"); }); test("Insert code via drag and drop", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor("#1\n#2\n#3\n"); - await app.findVisibleEditorContents("#2"); + await app.expectEditorContainText("#2"); await app.switchTab("Reference"); await app.selectDocumentationSection("Display"); await app.dragDropCodeEmbed("Scroll", 2); @@ -59,7 +57,7 @@ test.describe("documentation", () => { const expected = "from microbit import *display.scroll('score') display.scroll(23)#1#2#3"; - await app.findVisibleEditorContents(expected); + await app.expectEditorContainText(expected); }); test("Searches and navigates to the first result", async ({ app }) => { diff --git a/src/e2e/edits.test.ts b/src/e2e/edits.test.ts index e6fec3fb8..f5f036bf0 100644 --- a/src/e2e/edits.test.ts +++ b/src/e2e/edits.test.ts @@ -12,7 +12,7 @@ test.describe("edits", () => { test("prompts on close if file edited", async ({ app }) => { await app.typeInEditor("A change!"); - await app.findVisibleEditorContents(/A change/); + await app.expectEditorContainText(/A change/); await app.closePageCheckDialog(true); }); @@ -20,17 +20,17 @@ test.describe("edits", () => { test("prompts on close if project name edited", async ({ app }) => { const name = "idiosyncratic ruminant"; await app.setProjectName(name); - await app.findProjectName(name); + await app.expectProjectName(name); await app.closePageCheckDialog(true); }); test("retains text across a reload via session storage", async ({ app }) => { await app.typeInEditor("A change!"); - await app.findVisibleEditorContents(/A change/); + await app.expectEditorContainText(/A change/); await app.page.reload(); - await app.findVisibleEditorContents(/A change/); + await app.expectEditorContainText(/A change/); }); }); diff --git a/src/e2e/migration.test.ts b/src/e2e/migration.test.ts index a81e23c9f..8bae53368 100644 --- a/src/e2e/migration.test.ts +++ b/src/e2e/migration.test.ts @@ -15,8 +15,8 @@ test.describe("migration", () => { test("Loads the project from the URL", async ({ app }) => { await app.goto({ fragment: heartMigrationFragment }); await app.page.reload(); - await app.findProjectName("Hearts"); - await app.findVisibleEditorContents( + await app.expectProjectName("Hearts"); + await app.expectEditorContainText( "from microbit import *display.show(Image.HEART)" ); @@ -28,6 +28,6 @@ test.describe("migration", () => { await app.page.reload(); // wait for page to load await app.saveButton.waitFor(); - await app.findVisibleEditorContents("display.read_light_level"); + await app.expectEditorContainText("display.read_light_level"); }); }); diff --git a/src/e2e/multiple-files.test.ts b/src/e2e/multiple-files.test.ts index c99ca9cef..95c4352e3 100644 --- a/src/e2e/multiple-files.test.ts +++ b/src/e2e/multiple-files.test.ts @@ -21,7 +21,7 @@ test.describe("multiple-files", () => { test("Add a new file", async ({ app }) => { await app.createNewFile("test"); - await app.findVisibleEditorContents(/Your new file/); + await app.expectEditorContainText(/Your new file/); await app.findProjectFiles(["main.py", "test.py"]); }); @@ -34,13 +34,13 @@ test.describe("multiple-files", () => { acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, }); await app.switchToEditing("usermodule.py"); - await app.findVisibleEditorContents(/b_works/); + await app.expectEditorContainText(/b_works/); await app.loadFiles("testData/updated/usermodule.py", { acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, }); - await app.findVisibleEditorContents(/c_works/); + await app.expectEditorContainText(/c_works/); }); test("Shows warning for third-party module", async ({ app }) => { @@ -52,7 +52,7 @@ test.describe("multiple-files", () => { await app.toggleSettingThirdPartyModuleEditing(); try { - await app.findVisibleEditorContents(/a_works/); + await app.expectEditorContainText(/a_works/); } finally { await app.toggleSettingThirdPartyModuleEditing(); } @@ -71,7 +71,7 @@ test.describe("multiple-files", () => { await app.deleteFile("module.py"); - await app.findVisibleEditorContents(/Hello/); + await app.expectEditorContainText(/Hello/); }); test("Muddles through if given non-UTF-8 main.py", async ({ app }) => { @@ -79,7 +79,7 @@ test.describe("multiple-files", () => { // If we need to recreate the hex then just fill the file with 0xff. await app.loadFiles("testData/invalid-utf-8.hex"); - await app.findVisibleEditorContents( + await app.expectEditorContainText( /^����������������������������������������������������������������������������������������������������$/ ); }); diff --git a/src/e2e/open.test.ts b/src/e2e/open.test.ts index b3259f2a4..1bae75916 100644 --- a/src/e2e/open.test.ts +++ b/src/e2e/open.test.ts @@ -23,7 +23,7 @@ test.describe("open", () => { }); await app.findAlertText("Updated file main.py"); - await app.findProjectName("Untitled project"); + await app.expectProjectName("Untitled project"); }); test("Correctly handles a hex that's actually Python", async ({ app }) => { @@ -42,21 +42,21 @@ test.describe("open", () => { test("Loads a v1.0.1 hex file", async ({ app }) => { await app.loadFiles("testData/1.0.1.hex"); - await app.findVisibleEditorContents(/PASS1/); - await app.findProjectName("1.0.1"); + await app.expectEditorContainText(/PASS1/); + await app.expectProjectName("1.0.1"); }); test("Loads a v0.9 hex file", async ({ app }) => { await app.loadFiles("testData/0.9.hex"); - await app.findVisibleEditorContents(/PASS2/); - await app.findProjectName("0.9"); + await app.expectEditorContainText(/PASS2/); + await app.expectProjectName("0.9"); }); test("Loads via drag and drop", async ({ app }) => { await app.dropFile("testData/1.0.1.hex"); - await app.findProjectName("1.0.1"); + await app.expectProjectName("1.0.1"); // await app.findVisibleEditorContents(/PASS1/); }); @@ -103,8 +103,8 @@ test.describe("open", () => { await app.loadFiles("testData/1.0.1.hex", { acceptDialog: LoadDialogType.REPLACE, }); - await app.findVisibleEditorContents(/PASS1/); - await app.findProjectName("1.0.1"); + await app.expectEditorContainText(/PASS1/); + await app.expectProjectName("1.0.1"); }); test("No warn before load if you save hex", async ({ app }) => { @@ -115,7 +115,7 @@ test.describe("open", () => { // No dialog accepted await app.loadFiles("testData/1.0.1.hex"); - await app.findVisibleEditorContents(/PASS1/); + await app.expectEditorContainText(/PASS1/); }); test("No warn before load if you save main file", async ({ app }) => { @@ -125,7 +125,7 @@ test.describe("open", () => { // No dialog accepted await app.loadFiles("testData/1.0.1.hex"); - await app.findVisibleEditorContents(/PASS1/); + await app.expectEditorContainText(/PASS1/); }); test("Warn before load if you save main file only and you have others", async ({ @@ -140,6 +140,6 @@ test.describe("open", () => { await app.loadFiles("testData/1.0.1.hex", { acceptDialog: LoadDialogType.REPLACE, }); - await app.findVisibleEditorContents(/PASS1/); + await app.expectEditorContainText(/PASS1/); }); }); diff --git a/src/e2e/reset.test.ts b/src/e2e/reset.test.ts index 66c99048c..106bc02a2 100644 --- a/src/e2e/reset.test.ts +++ b/src/e2e/reset.test.ts @@ -15,8 +15,8 @@ test.describe("reset", () => { await app.resetProject(); // Everything's back to normal. - await app.findProjectName("Untitled project"); - await app.findVisibleEditorContents("from microbit import"); + await app.expectProjectName("Untitled project"); + await app.expectEditorContainText("from microbit import"); await app.findProjectFiles(["main.py"]); }); }); diff --git a/src/e2e/save.test.ts b/src/e2e/save.test.ts index 01902b1da..f683c441d 100644 --- a/src/e2e/save.test.ts +++ b/src/e2e/save.test.ts @@ -34,7 +34,7 @@ test.describe("save", () => { await app.loadFiles("testData/too-large.py", { acceptDialog: LoadDialogType.CONFIRM, }); - await app.findVisibleEditorContents(/# Filler/); + await app.expectEditorContainText(/# Filler/); await app.save({ waitForDownload: false }); await app.findAlertText( diff --git a/src/e2e/settings.test.ts b/src/e2e/settings.test.ts index 71853d977..147665e01 100644 --- a/src/e2e/settings.test.ts +++ b/src/e2e/settings.test.ts @@ -9,18 +9,18 @@ test.describe("settings", () => { test("sets language via URL", async ({ app }) => { await app.goto({ language: "fr" }); // French via the URL - await app.findProjectName("Projet sans titre"); + await app.expectProjectName("Projet sans titre"); await app.switchLanguage("en"); await app.page.reload(); // French URL ignored as we've made an explicit language choice. - await app.findProjectName("Untitled project"); + await app.expectProjectName("Untitled project"); }); test("switches language", async ({ app }) => { await app.switchLanguage("fr"); - await app.findProjectName("Projet sans titre"); + await app.expectProjectName("Projet sans titre"); await app.switchLanguage("en"); - await app.findProjectName("Untitled project"); + await app.expectProjectName("Untitled project"); }); }); From a6c75dd50aa509e374786baccd3dc26cbd52ce13 Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 18 Mar 2024 09:24:20 +0000 Subject: [PATCH 17/38] Update package-lock.json --- package-lock.json | 542 +++++++++++++++++++--------------------------- 1 file changed, 226 insertions(+), 316 deletions(-) diff --git a/package-lock.json b/package-lock.json index a49a1721a..f3a33c434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1793,9 +1793,9 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.15.0.tgz", - "integrity": "sha512-G2Zm0mXznxz97JhaaOdoEG2cVupn4JjPaS4AcNvZzhOsnnG9YVN68VzfoUw6dYTsIxT6a/cmoFEN47KAWhXaOg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.14.0.tgz", + "integrity": "sha512-Kx9BCSOLKmqNXEvmViuzsBQJ2VEa/wWwOATNpixOa+suttTV3rDnAUtAIt5ObAUFjXvZakWfFfF/EbxELnGLzQ==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -1851,9 +1851,9 @@ "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" }, "node_modules/@codemirror/view": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.0.tgz", - "integrity": "sha512-nSSmzONpqsNzshPOxiKhK203R6BvABepugAe34QfQDbNDslyjkqBuKgrK5ZBvqNXpfxz5iLrlGTmEfhbQyH46A==", + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.25.1.tgz", + "integrity": "sha512-2LXLxsQnHDdfGzDvjzAwZh2ZviNJm7im6tGpa0IONIDnFd8RZ80D2SNi8PDi6YjKcMoMRK20v6OmKIdsrwsyoQ==", "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", @@ -2412,16 +2412,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2437,18 +2427,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -2578,28 +2556,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2800,9 +2756,9 @@ } }, "node_modules/@lezer/python": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.12.tgz", - "integrity": "sha512-jDfUgOIDulv94R89dtYBfmIpCHiKn6RkeeVT7RQmbaKehJEMp30Bj5fHdAsgA2p8Gqjj+mbHVR+jyxUzSUNaOg==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.11.tgz", + "integrity": "sha512-C3QeLCcdAKJDUOsYjfFP6a1wdn8jhUNX200bgFm8TpKH1eM2PlgYQS5ugw6E38qGeEx7CP21I1Q52SoybXt0OQ==", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -2899,6 +2855,20 @@ "fsevents": "2.3.2" } }, + "node_modules/@playwright/test/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/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -3777,9 +3747,9 @@ "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==" }, "node_modules/@types/node": { - "version": "20.11.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz", - "integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==", + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -3803,9 +3773,9 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { - "version": "18.2.66", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz", - "integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==", + "version": "18.2.65", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.65.tgz", + "integrity": "sha512-98TsY0aW4jqx/3RqsUXwMDZSWR1Z4CUlJNue8ueS2/wcxZOsz4xmW1X8ieaWVRHcmmQM3R8xVA4XWB3dJnWwDQ==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3813,9 +3783,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.22", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", - "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", + "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", "dependencies": { "@types/react": "*" } @@ -4070,6 +4040,15 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4082,6 +4061,21 @@ "node": ">=10" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -4204,13 +4198,13 @@ } }, "node_modules/@vitest/expect": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz", - "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", "dev": true, "dependencies": { - "@vitest/spy": "1.4.0", - "@vitest/utils": "1.4.0", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "chai": "^4.3.10" }, "funding": { @@ -4218,12 +4212,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz", - "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", "dev": true, "dependencies": { - "@vitest/utils": "1.4.0", + "@vitest/utils": "1.3.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -4246,22 +4240,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@vitest/snapshot": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz", - "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -4305,9 +4287,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz", - "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -4317,9 +4299,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz", - "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -4738,15 +4720,12 @@ ] }, "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { @@ -4761,12 +4740,13 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -5593,9 +5573,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.707", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.707.tgz", - "integrity": "sha512-qRq74Mo7ChePOU6GHdfAJ0NREXU8vQTlVlfWz3wNygFay6xrd/fY2J7oGHwrhFeU30OVctGLdTh/FcnokTWpng==", + "version": "1.4.701", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", + "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==", "dev": true }, "node_modules/end-of-stream": { @@ -5954,16 +5934,6 @@ "eslint": ">=7" } }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5976,18 +5946,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -6048,16 +6006,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6092,6 +6040,22 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6128,16 +6092,49 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "p-locate": "^5.0.0" }, "engines": { - "node": "*" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/supports-color": { @@ -6152,6 +6149,18 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -6374,6 +6383,15 @@ "minimatch": "^5.0.1" } }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -6404,19 +6422,16 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", + "locate-path": "^5.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/flat-cache": { @@ -6537,9 +6552,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -6693,28 +6708,6 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7505,16 +7498,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/jake/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7558,18 +7541,6 @@ "node": ">=8" } }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/jake/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8203,18 +8174,15 @@ } }, "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/lodash": { @@ -8410,18 +8378,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/mkdirp-classic": { @@ -8747,33 +8712,30 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "dependencies": { - "yocto-queue": "^0.1.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "dependencies": { - "p-limit": "^3.0.2" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/p-try": { @@ -8915,58 +8877,6 @@ "node": ">=8" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", @@ -9007,6 +8917,19 @@ "node": ">=14" } }, + "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==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/playwright/node_modules/playwright-core": { "version": "1.42.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", @@ -9558,9 +9481,9 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", + "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -10504,9 +10427,9 @@ } }, "node_modules/ufo": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.0.tgz", - "integrity": "sha512-c7SxU8XB0LTO7hALl6CcE1Q92ZrLzr1iE0IVIsUa9SlFfkn2B2p6YLO6dLxOj7qCWY98PB3Q3EZbN6bEu8p7jA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", "dev": true }, "node_modules/unbox-primitive": { @@ -10699,9 +10622,9 @@ } }, "node_modules/vite-node": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz", - "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -11101,30 +11024,17 @@ "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/vitest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz", - "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", "dev": true, "dependencies": { - "@vitest/expect": "1.4.0", - "@vitest/runner": "1.4.0", - "@vitest/snapshot": "1.4.0", - "@vitest/spy": "1.4.0", - "@vitest/utils": "1.4.0", + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -11138,7 +11048,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.4.0", + "vite-node": "1.3.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -11153,8 +11063,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.4.0", - "@vitest/ui": "1.4.0", + "@vitest/browser": "1.3.1", + "@vitest/ui": "1.3.1", "happy-dom": "*", "jsdom": "*" }, @@ -11462,12 +11372,12 @@ } }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" From 627a0e0c7313a2bc01885598f8522f142c2d7764 Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 18 Mar 2024 10:44:55 +0000 Subject: [PATCH 18/38] Replace app.ts with app-playwright.ts --- src/e2e/app-playwright.ts | 813 -------------- src/e2e/app-test-fixtures.ts | 2 +- src/e2e/app.ts | 1813 ++++++++++---------------------- src/e2e/multiple-files.test.ts | 2 +- src/e2e/open.test.ts | 2 +- src/e2e/save.test.ts | 2 +- 6 files changed, 586 insertions(+), 2048 deletions(-) delete mode 100644 src/e2e/app-playwright.ts diff --git a/src/e2e/app-playwright.ts b/src/e2e/app-playwright.ts deleted file mode 100644 index eb4294346..000000000 --- a/src/e2e/app-playwright.ts +++ /dev/null @@ -1,813 +0,0 @@ -/** - * (c) 2021 - 2022, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { BrowserContext, Frame, Locator, Page, expect } from "@playwright/test"; -import { Flag } from "../flags"; -import path from "path"; -import { fileURLToPath } from "url"; -import { readFileSync } from "fs"; -import { WebUSBErrorCode } from "../device/device"; - -export enum LoadDialogType { - CONFIRM, - REPLACE, - CONFIRM_BUT_LOAD_AS_MODULE, - NONE, -} - -export interface BrowserDownload { - filename: string; - data: Buffer; -} - -const baseUrl = "http://localhost:3000"; - -interface UrlOptions { - flags?: Flag[]; - fragment?: string; - language?: string; -} - -interface SaveOptions { - waitForDownload: boolean; -} - -class LoadDialog { - private confirmButton: Locator; - private replaceButton: Locator; - private optionsButton: Locator; - private type: LoadDialogType; - - constructor(public readonly page: Page, type: LoadDialogType) { - this.type = type; - this.confirmButton = this.page.getByRole("button", { name: "Confirm" }); - this.replaceButton = this.page.getByRole("button", { name: "Replace" }); - this.optionsButton = this.page.getByRole("button", { - name: "Options", - exact: true, - }); - } - - async submit() { - switch (this.type) { - case LoadDialogType.CONFIRM: - return await this.confirmButton.click(); - case LoadDialogType.REPLACE: - return await this.replaceButton.click(); - case LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE: - await this.optionsButton.click(); - await this.page.getByText(/^(Add|Replace) file .+\.py$/).click(); - return await this.confirmButton.click(); - default: - return; - } - } -} - -class FileActionsMenu { - public saveButton: Locator; - public editButton: Locator; - public deleteButton: Locator; - - constructor(public readonly page: Page, filename: string) { - this.saveButton = this.page.getByRole("menuitem", { - name: `Save ${filename}`, - }); - this.editButton = this.page.getByRole("menuitem", { - name: `Edit ${filename}`, - }); - this.deleteButton = this.page.getByRole("menuitem", { - name: `Delete ${filename}`, - }); - } - - async delete() { - await this.deleteButton.waitFor(); - await this.deleteButton.click(); - await this.page.getByRole("button", { name: "Delete" }).click(); - } -} - -class ProjectTabPanel { - private openButton: Locator; - constructor(public readonly page: Page) { - this.openButton = this.page - .getByRole("tabpanel", { name: "Project" }) - .getByTestId("open"); - } - - async openFileActionsMenu(filename: string) { - const fileActionsMenu = this.page.getByRole("button", { - name: `${filename} file actions`, - }); - await fileActionsMenu.waitFor(); - await fileActionsMenu.click(); - return new FileActionsMenu(this.page, filename); - } - - async chooseFile(filePathFromProjectRoot: string) { - const filePath = getAbsoluteFilePath(filePathFromProjectRoot); - const fileChooserPromise = this.page.waitForEvent("filechooser"); - await this.openButton.click(); - const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(filePath); - } -} - -class SideBar { - public expandButton: Locator; - public collapseButton: Locator; - - constructor(public readonly page: Page) { - this.expandButton = this.page.getByLabel("Expand sidebar"); - this.collapseButton = this.page.getByLabel("Collapse sidebar"); - } -} - -class Simulator { - public expandButton: Locator; - public collapseButton: Locator; - - constructor(public readonly page: Page) { - this.expandButton = this.page.getByLabel("Expand simulator"); - this.collapseButton = this.page.getByLabel("Collapse simulator"); - } -} - -export class App { - public editorTextArea: Locator; - private settingsButton: Locator; - public saveButton: Locator; - private searchButton: Locator; - public modifierKey: string; - public projectTab: ProjectTabPanel; - private moreConnectionOptionsButton: Locator; - public baseUrl: string; - private editor: Locator; - public simulator: Simulator; - public sidebar: SideBar; - - constructor(public readonly page: Page, public context: BrowserContext) { - this.baseUrl = baseUrl; - this.editor = this.page.getByTestId("editor"); - this.editorTextArea = this.editor.getByRole("textbox"); - this.projectTab = new ProjectTabPanel(page); - this.settingsButton = this.page.getByTestId("settings"); - this.saveButton = this.page.getByRole("button", { - name: "Save", - exact: true, - }); - this.searchButton = this.page.getByRole("button", { name: "Search" }); - this.moreConnectionOptionsButton = this.page.getByTestId( - "more-connect-options" - ); - this.simulator = new Simulator(this.page); - this.sidebar = new SideBar(this.page); - - // Set modifier key - const isMac = process.platform === "darwin"; - this.modifierKey = isMac ? "Meta" : "Control"; - } - - async goto(options: UrlOptions = {}) { - await this.page.goto(optionsToURL(options)); - // Wait for the page to be loaded - await this.editor.waitFor(); - } - - async setProjectName(projectName: string): Promise { - await this.page.getByRole("button", { name: "Edit project name" }).click(); - await this.page.getByLabel("Name*").fill(projectName); - await this.page.getByRole("button", { name: "Confirm" }).click(); - } - - async expectProjectName(match: string) { - await expect(this.page.getByTestId("project-name")).toHaveText(match); - } - - async switchLanguage(locale: string) { - // All test ids so they can be language invariant. - await this.settingsButton.click(); - await this.page.getByTestId("language").click(); - await this.page.getByTestId(locale).click(); - } - - async selectAllInEditor(): Promise { - await this.editorTextArea.click(); - await this.page.keyboard.press(`${this.modifierKey}+A`); - } - - // TODO: Rename to pasteInEditor - async pasteInEditor() { - // Simulating keyboard press CTRL+V works in Playwright, - // but does not work in this case potentially due to - // CodeMirror pasting magic - const clipboardText: string = await this.page.evaluate( - "navigator.clipboard.readText()" - ); - await this.editorTextArea.evaluate((el, text) => { - const clipboardData = new DataTransfer(); - clipboardData.setData("text/plain", text); - const clipboardEvent = new ClipboardEvent("paste", { clipboardData }); - el.dispatchEvent(clipboardEvent); - }, clipboardText); - } - - async typeInEditor(text: string): Promise { - const textWithoutLastChar = text.slice(0, text.length - 1); - await this.editorTextArea.fill(textWithoutLastChar); - // Last character is typed separately to trigger editor suggestions - await this.page.keyboard.press(text.slice(-1)); - } - - async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { - await this.page.getByRole("tab", { name: tabName }).click(); - } - - async createNewFile(name: string): Promise { - await this.switchTab("Project"); - await this.page.getByRole("button", { name: "Create file" }).click(); - await this.page.getByLabel("Name*").fill(name); - await this.page.getByRole("button", { name: "Create" }).click(); - } - - async resetProject(): Promise { - await this.switchTab("Project"); - await this.page.getByRole("button", { name: "Reset project" }).click(); - await this.page.getByRole("button", { name: "Replace" }).click(); - } - - // Use allInnerTexts() for matching text - async expectEditorContainText(match: RegExp | string) { - // Scroll to the top of code text area - await this.editorTextArea.click(); - await this.page.mouse.wheel(0, -100000000); - await expect(this.editorTextArea).toContainText(match); - } - - // TODO: Rename to expectProjectFiles - async findProjectFiles(expected: string[]): Promise { - await this.switchTab("Project"); - await expect(this.page.getByRole("listitem")).toHaveText(expected); - } - - async loadFiles( - filePathFromProjectRoot: string, - options: { acceptDialog?: LoadDialogType } = {} - ) { - await this.switchTab("Project"); - await this.projectTab.chooseFile(filePathFromProjectRoot); - - if (options.acceptDialog !== undefined) { - const loadDialog = new LoadDialog(this.page, options.acceptDialog); - await loadDialog.submit(); - } - } - - async dropFile( - filePathFromProjectRoot: string, - options: { acceptDialog?: LoadDialogType } = {} - ) { - const filePath = getAbsoluteFilePath(filePathFromProjectRoot); - const filename = getFilename(filePathFromProjectRoot); - - // wait for page to load - await this.saveButton.waitFor(); - - // Playwright drag and drop file method taken from - // https://github.com/microsoft/playwright/issues/10667#issuecomment-998397241 - const buffer = readFileSync(filePath, { encoding: "ascii" }); - const dataTransfer = await this.page.evaluateHandle( - ({ buffer, filename }) => { - const dt = new DataTransfer(); - const file = new File([buffer], filename); - dt.items.add(file); - return dt; - }, - { buffer, filename } - ); - - // Drag file over target area to reveal drop zone - await this.page - .getByTestId("project-drop-target") - .dispatchEvent("dragover", { dataTransfer }); - - const dropZone = this.page.getByTestId("project-drop-target-overlay"); - await dropZone.waitFor(); - await dropZone.dispatchEvent("drop", { dataTransfer }); - - if (options.acceptDialog !== undefined) { - const loadDialog = new LoadDialog(this.page, options.acceptDialog); - await loadDialog.submit(); - } - } - - // TODO: Rename to expectAlertText - async findAlertText(title: string, description?: string): Promise { - await expect(this.page.getByText(title)).toBeVisible(); - if (description) { - await expect(this.page.getByText(description)).toBeVisible(); - } - } - - async isDeleteFileOptionDisabled(filename: string) { - await this.switchTab("Project"); - const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); - return await fileOptionMenu.deleteButton.isDisabled(); - } - - async isEditFileOptionDisabled(filename: string) { - await this.switchTab("Project"); - const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); - return await fileOptionMenu.editButton.isDisabled(); - } - - // TODO: Rename to editFile - async switchToEditing(filename: string): Promise { - await this.switchTab("Project"); - const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); - await fileOptionMenu.editButton.click(); - } - - async findThirdPartyModuleWarning( - expectedName: string, - expectedVersion: string - ): Promise { - for (const name in [expectedName, expectedVersion]) { - await expect(this.page.getByRole("cell", { name })).toBeVisible(); - } - } - - async closeDialog(dialogText?: string) { - if (dialogText) { - await this.page.getByText(dialogText).waitFor(); - } - await this.page.getByRole("button", { name: "Close" }).first().click(); - } - - async save(options: SaveOptions = { waitForDownload: true }) { - if (!options.waitForDownload) { - await this.saveButton.click(); - return; - } - const downloadPromise = this.page.waitForEvent("download"); - await this.saveButton.click(); - return await downloadPromise; - } - - // TODO: Rename to savePythonScript - async saveMain() { - await this.page.getByTestId("more-save-options").click(); - const downloadPromise = this.page.waitForEvent("download"); - await this.page - .getByRole("menuitem", { name: "Save Python script" }) - .click(); - await downloadPromise; - } - - // TODO: Rename to expectDialog - async confirmInputDialog(text: string) { - await expect(this.page.getByText(text)).toBeVisible(); - } - - async deleteFile(filename: string) { - await this.switchTab("Project"); - const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); - await fileOptionMenu.delete(); - } - - async toggleSettingThirdPartyModuleEditing(): Promise { - await this.settingsButton.click(); - await this.page.getByRole("menuitem", { name: "Settings" }).click(); - await this.page - .getByText("Allow editing third-party modules", { exact: true }) - .click(); - await this.page.getByRole("button", { name: "Close" }).click(); - } - - // Rename to closeAndExpectBeforeUnloadDialogVisible - async closePageCheckDialog(visible: boolean): Promise { - this.page.on("dialog", async (dialog) => { - expect(dialog.type() === "beforeunload").toEqual(visible); - await dialog.dismiss(); - }); - this.page.close({ runBeforeUnload: true }); - } - - // Rename to expectDocumentationTopLevelHeading - async findDocumentationTopLevelHeading( - title: string, - description?: string - ): Promise { - await expect( - this.page.getByRole("heading", { name: title, exact: true }) - ).toBeVisible(); - if (description) { - await expect(this.page.getByText(description)).toBeVisible(); - } - } - - async selectDocumentationSection(name: string): Promise { - await this.page.getByRole("heading", { name }).click(); - } - - async toggleCodeActionButton(name: string): Promise { - await this.page - .getByRole("listitem") - .filter({ hasText: name }) - .getByRole("button", { name: "More" }) - .click(); - } - - async selectToolkitDropDownOption( - label: string, - option: string - ): Promise { - await this.page.getByRole("combobox", { name: label }).selectOption(option); - } - - private getCodeExample(name: string) { - return this.page - .getByRole("listitem") - .filter({ hasText: name }) - .locator("div") - .filter({ - hasText: "Code example:", - }) - .nth(2); - } - - async copyCode(name: string) { - await this.getCodeExample(name).click(); - await this.page.getByRole("button", { name: "Copy code" }).click(); - } - - async dragDropCodeEmbed(name: string, targetLine: number) { - const codeExample = this.getCodeExample(name); - const editorLine = this.page - .getByTestId("editor") - .getByRole("textbox") - .locator("div") - .filter({ hasText: targetLine.toString() }); - - await codeExample.dragTo(editorLine); - } - - // TODO: Rename to search - async searchToolkits(searchText: string): Promise { - await this.switchTab("Reference"); - await this.searchButton.click(); - await this.page.getByPlaceholder("Search").fill(searchText); - } - - async selectFirstSearchResult(): Promise { - // wait for results to show - await this.page.getByRole("link").first().waitFor(); - const links = await this.page.getByRole("link").all(); - await links[0].click(); - } - - async selectDocumentationIdea(name: string): Promise { - await this.page.getByRole("button", { name }).click(); - } - - async connect(): Promise { - await this.moreConnectionOptionsButton.click(); - await this.page.getByRole("menuitem", { name: "Connect" }).click(); - await this.connectViaConnectHelp(); - } - - // Connects from the connect dialog/wizard. - async connectViaConnectHelp(): Promise { - await this.page.getByRole("button", { name: "Next" }).click(); - await this.page.getByRole("button", { name: "Next" }).click(); - } - - // TODO: Extract as variable instead of function - private findMainSerialArea() { - return this.page.getByRole("region", { - name: "Serial terminal", - exact: true, - }); - } - - async confirmConnection(): Promise { - const serialMenu = this.findMainSerialArea().getByRole("button", { - name: "Serial menu", - }); - await expect(serialMenu).toBeVisible(); - } - - // TODO: Move expect out to separate function - async disconnect(): Promise { - await this.moreConnectionOptionsButton.click(); - await this.page.getByRole("menuitem", { name: "Disconnect" }).click(); - const btns = await this.page - .getByRole("button", { name: "Serial terminal" }) - .all(); - - expect(btns.length).toEqual(0); - } - - async serialShow(): Promise { - await this.findMainSerialArea() - .getByRole("button", { name: "Show serial" }) - .click(); - - // TODO: Extract - // Make sure the button has flipped. - const hideSerialButton = this.findMainSerialArea().getByRole("button", { - name: "Hide serial", - }); - await expect(hideSerialButton).toBeVisible(); - } - - async serialHide(): Promise { - await this.findMainSerialArea() - .getByRole("button", { name: "Hide serial" }) - .click(); - - // TODO: Extract - // Make sure the button has flipped. - const showSerialButton = this.findMainSerialArea().getByRole("button", { - name: "Show serial", - }); - await expect(showSerialButton).toBeVisible(); - } - - async flash() { - await this.page.getByRole("button", { name: "Send to micro:bit" }).click(); - } - - async mockSerialWrite(data: string): Promise { - this.page.evaluate((data) => { - (window as any).mockDevice.mockSerialWrite(data); - }, toCrLf(data)); - } - - async followSerialCompactTracebackLink(): Promise { - await this.page.getByTestId("traceback-link").click(); - } - - async mockDeviceConnectFailure(code: WebUSBErrorCode) { - this.page.evaluate((code) => { - (window as any).mockDevice.mockConnect(code); - }, code); - } - - async findSerialCompactTraceback(text: string | RegExp): Promise { - await expect(this.page.getByText(text)).toBeVisible(); - } - - // Retry micro:bit connection from error dialogs. - async connectViaTryAgain(): Promise { - await this.page.getByRole("button", { name: "Try again" }).click(); - } - - // Launch 'connect help' dialog from 'not found' dialog. - async connectHelpFromNotFoundDialog(): Promise { - await this.page.getByRole("link", { name: "follow these steps" }).click(); - } - - async mockWebUsbNotSupported() { - this.page.evaluate(() => { - (window as any).mockDevice.mockWebUsbNotSupported(); - }); - } - - // TODO: Rename to expectCompletionOptions - // try toContainText instead for testing! - async findCompletionOptions(expected: string[]): Promise { - const completions = this.page.getByRole("listbox", { name: "Completions" }); - const contents = await completions.innerText(); - - expect(contents).toEqual(expected.join("\n")); - } - - // TODO: Rename to expectCompletionActiveOption - async findCompletionActiveOption(signature: string): Promise { - const activeOption = this.editor - .locator("div") - .filter({ hasText: signature }) - .nth(2); - await expect(activeOption).toBeVisible(); - } - - async acceptCompletion(name: string): Promise { - // This seems significantly more reliable than pressing Enter, though there's - // no real-life issue here. - await this.editor.getByRole("option", { name }).click(); - } - - async followCompletionOrSignatureDocumentionLink( - linkName: "Help" | "API" - ): Promise { - await this.page.getByRole("link", { name: linkName }).click(); - } - - // TODO: Rename to expectActiveApiEntry - async findActiveApiEntry(text: string, _headingLevel: string): Promise { - // We need to make sure it's actually visible as it's scroll-based navigation. - await expect(this.page.getByRole("heading", { name: text })).toBeVisible(); - } - - // TODO: Rename to expectSignatureHelp - async findSignatureHelp(expectedSignature: string): Promise { - const signatureHelp = this.editor - .locator("div") - .filter({ hasText: expectedSignature }) - .nth(1); - await signatureHelp.waitFor(); - await expect(signatureHelp).toBeVisible(); - } - - // Simulator functions - private getSimulatorIframe(): Frame { - const simulatorIframe = this.page - .frames() - .find((frame) => frame.name() === "Simulator"); - if (!simulatorIframe) { - throw new Error("Simulator iframe not found"); - } - return simulatorIframe; - } - - async runSimulator(): Promise { - const simulatorIframe = this.getSimulatorIframe(); - const playButton = simulatorIframe.locator(".play-button"); - await playButton.click(); - } - - async simulatorSelectGesture(option: string): Promise { - await this.page - .getByTestId("simulator-gesture-select") - .selectOption(option); - } - - async simulatorSendGesture(): Promise { - await this.page.getByRole("button", { name: "Send gesture" }).click(); - } - - async simulatorConfirmResponse(): Promise { - // Confirms that top left LED is switched on - // to match Image.NO being displayed. - const gridLEDs = this.getSimulatorIframe().locator("#LEDsOn"); - await expect(gridLEDs).toBeVisible(); - } - - async simulatorSetRangeSlider( - sliderLabel: string, - value: "min" | "max" - ): Promise { - const sliderThumb = this.page.locator( - `[role="slider"][aria-label="${sliderLabel}"]` - ); - const bounding_box = await sliderThumb!.boundingBox(); - await this.page.mouse.move( - bounding_box!.x + bounding_box!.width / 2, - bounding_box!.y + bounding_box!.height / 2 - ); - await this.page.mouse.down(); - await this.page.waitForTimeout(500); - await this.page.mouse.move(value === "max" ? 1200 : 0, 0); - await this.page.waitForTimeout(500); - await this.page.mouse.up(); - } - - async simulatorInputPressHold( - name: string, - pressDuration: number - ): Promise { - const inputButton = this.page.getByRole("button", { - name, - }); - const bounding_box = await inputButton!.boundingBox(); - await this.page.mouse.move( - bounding_box!.x + bounding_box!.width / 2, - bounding_box!.y + bounding_box!.height / 2 - ); - await this.page.mouse.down(); - await this.page.waitForTimeout(pressDuration); - await this.page.mouse.up(); - } - - async findStoppedSimulator(): Promise { - const button = this.page.getByRole("button", { - name: "Stop simulator", - }); - expect(await button.isDisabled()).toEqual(true); - } - - // TODO: Rename to expectFocusOnLoad - async assertFocusOnLoad(): Promise { - const link = this.page.getByLabel( - "visit microbit.org (opens in a new tab)" - ); - await this.page.keyboard.press("Tab"); - await expect(link).toBeFocused(); - } - - async collapseSimulator(): Promise { - await this.simulator.collapseButton.click(); - } - - async expandSimulator(): Promise { - await this.simulator.expandButton.click(); - } - - async collapseSidebar(): Promise { - await this.sidebar.collapseButton.click(); - } - - async expandSidebar(): Promise { - await this.sidebar.expandButton.click(); - } - - async assertFocusOnExpandSimulator(): Promise { - await expect(this.simulator.expandButton).toBeFocused(); - } - - async assertFocusOnSimulator(): Promise { - const simulator = this.page.locator("iframe[name='Simulator']"); - await expect(simulator).toBeFocused(); - } - - async assertFocusOnExpandSidebar(): Promise { - await expect(this.sidebar.expandButton).toBeFocused(); - } - - async assertFocusOnSidebar(): Promise { - const simulator = this.page.getByRole("tabpanel", { name: "Reference" }); - await expect(simulator).toBeFocused(); - } - - async assertFocusBeforeEditor(): Promise { - const zoomIn = this.page.getByRole("button", { - name: "Zoom in", - }); - await expect(zoomIn).toBeFocused(); - } - - async assertFocusAfterEditor(): Promise { - const sendButton = this.page.getByRole("button", { - name: "Send to micro:bit", - }); - await expect(sendButton).toBeFocused(); - } - - async tabOutOfEditorForwards(): Promise { - await this.editor.click(); - await this.page.keyboard.press("Escape"); - await this.page.keyboard.press("Tab"); - } - - async tabOutOfEditorBackwards(): Promise { - await this.editor.click(); - await this.page.keyboard.press("Escape"); - await this.page.keyboard.down("Shift"); - await this.page.keyboard.press("Tab"); - await this.page.keyboard.up("Shift"); - } -} - -const toCrLf = (text: string): string => - text.replace(/[\r\n]/g, "\n").replace(/\n/g, "\r\n"); - -export const getFilename = (filePath: string) => { - const filename = filePath.split("/").pop(); - if (!filename) { - throw new Error("dropFile Error: No filename found!"); - } - return filename; -}; - -const getAbsoluteFilePath = (filePathFromProjectRoot: string) => { - const dir = path.dirname(fileURLToPath(import.meta.url)); - return path.join(dir.replace("src/e2e", ""), filePathFromProjectRoot); -}; - -const optionsToURL = (options: UrlOptions): string => { - const flags = new Set([ - "none", - "noWelcome", - ...(options.flags ?? []), - ]); - const params: Array<[string, string]> = Array.from(flags).map((f) => [ - "flag", - f, - ]); - if (options.language) { - params.push(["l", options.language]); - } - return ( - baseUrl + - // We didn't use BASE_URL here as CRA seems to set it to "" before running jest. - // Maybe can be changed since the Vite upgrade. - (process.env.E2E_BASE_URL ?? "/") + - "?" + - new URLSearchParams(params) + - (options.fragment ?? "") - ); -}; diff --git a/src/e2e/app-test-fixtures.ts b/src/e2e/app-test-fixtures.ts index e17ba7d91..2d84dda53 100644 --- a/src/e2e/app-test-fixtures.ts +++ b/src/e2e/app-test-fixtures.ts @@ -1,5 +1,5 @@ import { test as base } from "@playwright/test"; -import { App } from "./app-playwright.js"; +import { App } from "./app.js"; type MyFixtures = { app: App; diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 796962071..eb4294346 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -3,24 +3,12 @@ * * SPDX-License-Identifier: MIT */ -import { waitFor, waitForOptions } from "@testing-library/dom"; -import { Matcher } from "@testing-library/react"; -import * as fs from "fs"; -import * as fsp from "fs/promises"; -import { escapeRegExp } from "lodash"; -import * as os from "os"; -import * as path from "path"; -import "pptr-testing-library/extend"; -import puppeteer, { - Browser, - Dialog, - ElementHandle, - Frame, - KeyInput, - Page, -} from "puppeteer"; -import { WebUSBErrorCode } from "../device/device"; +import { BrowserContext, Frame, Locator, Page, expect } from "@playwright/test"; import { Flag } from "../flags"; +import path from "path"; +import { fileURLToPath } from "url"; +import { readFileSync } from "fs"; +import { WebUSBErrorCode } from "../device/device"; export enum LoadDialogType { CONFIRM, @@ -34,1429 +22,792 @@ export interface BrowserDownload { data: Buffer; } -const defaultWaitForOptions = { timeout: 10_000 }; - const baseUrl = "http://localhost:3000"; -const reportsPath = "reports/e2e/"; - -interface Options { - /** - * Flags. - * - * "none" and "noWelcome" are always added. - * - * Do not use "*", instead explicitly enable the set of flags your test requires. - */ + +interface UrlOptions { flags?: Flag[]; - /** - * URL fragment including the #. - */ fragment?: string; - /** - * Language parameter passed via URL. - */ language?: string; } -/** - * Model of the app to drive it for e2e testing. - * - * We could split this into screen areas accessible from this class. - * - * All methods should ensure they wait for a condition rather than relying on timing. - * - * Generally this means it's better to pass in expected values, so you can wait for - * them to be true, than to read and return data from the DOM. - */ -export class App { - private url: string; - /** - * Tracks dialogs observed by Pupeteer's dialog event. - */ - private dialogs: string[] = []; - private browser: Promise; - private page: Promise; - private downloadPath = fs.mkdtempSync( - path.join(os.tmpdir(), "puppeteer-downloads-") - ); +interface SaveOptions { + waitForDownload: boolean; +} - constructor(options: Options = {}) { - this.url = this.optionsToURL(options); - this.browser = puppeteer.launch({ - headless: process.env.E2E_HEADLESS !== "0", - // Needs to be large enough to display Reference + Simulator or tests need to show/hide them. - defaultViewport: { width: 1920, height: 1440 }, +class LoadDialog { + private confirmButton: Locator; + private replaceButton: Locator; + private optionsButton: Locator; + private type: LoadDialogType; + + constructor(public readonly page: Page, type: LoadDialogType) { + this.type = type; + this.confirmButton = this.page.getByRole("button", { name: "Confirm" }); + this.replaceButton = this.page.getByRole("button", { name: "Replace" }); + this.optionsButton = this.page.getByRole("button", { + name: "Options", + exact: true, + }); + } + + async submit() { + switch (this.type) { + case LoadDialogType.CONFIRM: + return await this.confirmButton.click(); + case LoadDialogType.REPLACE: + return await this.replaceButton.click(); + case LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE: + await this.optionsButton.click(); + await this.page.getByText(/^(Add|Replace) file .+\.py$/).click(); + return await this.confirmButton.click(); + default: + return; + } + } +} + +class FileActionsMenu { + public saveButton: Locator; + public editButton: Locator; + public deleteButton: Locator; + + constructor(public readonly page: Page, filename: string) { + this.saveButton = this.page.getByRole("menuitem", { + name: `Save ${filename}`, + }); + this.editButton = this.page.getByRole("menuitem", { + name: `Edit ${filename}`, + }); + this.deleteButton = this.page.getByRole("menuitem", { + name: `Delete ${filename}`, }); - this.page = this.createPage(); } - setOptions(options: Options) { - this.url = this.optionsToURL(options); + async delete() { + await this.deleteButton.waitFor(); + await this.deleteButton.click(); + await this.page.getByRole("button", { name: "Delete" }).click(); } +} - private optionsToURL(options: Options): string { - const flags = new Set([ - "none", - "noWelcome", - ...(options.flags ?? []), - ]); - const params: Array<[string, string]> = Array.from(flags).map((f) => [ - "flag", - f, - ]); - if (options.language) { - params.push(["l", options.language]); - } - return ( - baseUrl + - // We didn't use BASE_URL here as CRA seems to set it to "" before running jest. - // Maybe can be changed since the Vite upgrade. - (process.env.E2E_BASE_URL ?? "/") + - "?" + - new URLSearchParams(params) + - (options.fragment ?? "") - ); +class ProjectTabPanel { + private openButton: Locator; + constructor(public readonly page: Page) { + this.openButton = this.page + .getByRole("tabpanel", { name: "Project" }) + .getByTestId("open"); } - async createPage() { - const browser = await this.browser; - const context = browser.defaultBrowserContext(); - const { origin } = new URL(this.url); - await context.overridePermissions(origin, [ - "clipboard-read", - "clipboard-write", - ]); - - const page = await context.newPage(); - await page.setCookie({ - // See corresponding code in App.tsx. - name: "mockDevice", - value: "1", - url: this.url, - }); - // Don't show compliance notice for Foundation builds - await page.setCookie({ - name: "MBCC", - value: encodeURIComponent( - JSON.stringify({ - version: 1, - analytics: false, - functional: true, - }) - ), - url: this.url, + async openFileActionsMenu(filename: string) { + const fileActionsMenu = this.page.getByRole("button", { + name: `${filename} file actions`, }); + await fileActionsMenu.waitFor(); + await fileActionsMenu.click(); + return new FileActionsMenu(this.page, filename); + } - const client = await page.target().createCDPSession(); - await client.send("Page.setDownloadBehavior", { - behavior: "allow", - downloadPath: this.downloadPath, - }); + async chooseFile(filePathFromProjectRoot: string) { + const filePath = getAbsoluteFilePath(filePathFromProjectRoot); + const fileChooserPromise = this.page.waitForEvent("filechooser"); + await this.openButton.click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(filePath); + } +} - this.dialogs.length = 0; - page.on("dialog", async (dialog: Dialog) => { - this.dialogs.push(dialog.type()); - // Need to accept() so that reload() will complete. - await dialog.accept(); - }); +class SideBar { + public expandButton: Locator; + public collapseButton: Locator; - const logsPath = this.reportFilename("txt"); - // Clears previous output from local file. - fs.writeFile(logsPath, "", (err) => { - if (err) { - // Log file error. - console.error("Log file error: ", err.message); - } - }); - page.on("console", (msg) => { - fs.appendFile(logsPath, msg.text() + "\n", (err) => { - if (err) { - // Log file error. - console.error("Log file error: ", err.message); - } - }); - }); + constructor(public readonly page: Page) { + this.expandButton = this.page.getByLabel("Expand sidebar"); + this.collapseButton = this.page.getByLabel("Collapse sidebar"); + } +} - await page.evaluate(() => { - if (document.domain === "localhost") { - window.localStorage.clear(); - } - }); +class Simulator { + public expandButton: Locator; + public collapseButton: Locator; + + constructor(public readonly page: Page) { + this.expandButton = this.page.getByLabel("Expand simulator"); + this.collapseButton = this.page.getByLabel("Collapse simulator"); + } +} + +export class App { + public editorTextArea: Locator; + private settingsButton: Locator; + public saveButton: Locator; + private searchButton: Locator; + public modifierKey: string; + public projectTab: ProjectTabPanel; + private moreConnectionOptionsButton: Locator; + public baseUrl: string; + private editor: Locator; + public simulator: Simulator; + public sidebar: SideBar; + + constructor(public readonly page: Page, public context: BrowserContext) { + this.baseUrl = baseUrl; + this.editor = this.page.getByTestId("editor"); + this.editorTextArea = this.editor.getByRole("textbox"); + this.projectTab = new ProjectTabPanel(page); + this.settingsButton = this.page.getByTestId("settings"); + this.saveButton = this.page.getByRole("button", { + name: "Save", + exact: true, + }); + this.searchButton = this.page.getByRole("button", { name: "Search" }); + this.moreConnectionOptionsButton = this.page.getByTestId( + "more-connect-options" + ); + this.simulator = new Simulator(this.page); + this.sidebar = new SideBar(this.page); - return page; + // Set modifier key + const isMac = process.platform === "darwin"; + this.modifierKey = isMac ? "Meta" : "Control"; + } + + async goto(options: UrlOptions = {}) { + await this.page.goto(optionsToURL(options)); + // Wait for the page to be loaded + await this.editor.waitFor(); + } + + async setProjectName(projectName: string): Promise { + await this.page.getByRole("button", { name: "Edit project name" }).click(); + await this.page.getByLabel("Name*").fill(projectName); + await this.page.getByRole("button", { name: "Confirm" }).click(); + } + + async expectProjectName(match: string) { + await expect(this.page.getByTestId("project-name")).toHaveText(match); + } + + async switchLanguage(locale: string) { + // All test ids so they can be language invariant. + await this.settingsButton.click(); + await this.page.getByTestId("language").click(); + await this.page.getByTestId(locale).click(); + } + + async selectAllInEditor(): Promise { + await this.editorTextArea.click(); + await this.page.keyboard.press(`${this.modifierKey}+A`); + } + + // TODO: Rename to pasteInEditor + async pasteInEditor() { + // Simulating keyboard press CTRL+V works in Playwright, + // but does not work in this case potentially due to + // CodeMirror pasting magic + const clipboardText: string = await this.page.evaluate( + "navigator.clipboard.readText()" + ); + await this.editorTextArea.evaluate((el, text) => { + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + const clipboardEvent = new ClipboardEvent("paste", { clipboardData }); + el.dispatchEvent(clipboardEvent); + }, clipboardText); + } + + async typeInEditor(text: string): Promise { + const textWithoutLastChar = text.slice(0, text.length - 1); + await this.editorTextArea.fill(textWithoutLastChar); + // Last character is typed separately to trigger editor suggestions + await this.page.keyboard.press(text.slice(-1)); + } + + async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { + await this.page.getByRole("tab", { name: tabName }).click(); + } + + async createNewFile(name: string): Promise { + await this.switchTab("Project"); + await this.page.getByRole("button", { name: "Create file" }).click(); + await this.page.getByLabel("Name*").fill(name); + await this.page.getByRole("button", { name: "Create" }).click(); + } + + async resetProject(): Promise { + await this.switchTab("Project"); + await this.page.getByRole("button", { name: "Reset project" }).click(); + await this.page.getByRole("button", { name: "Replace" }).click(); + } + + // Use allInnerTexts() for matching text + async expectEditorContainText(match: RegExp | string) { + // Scroll to the top of code text area + await this.editorTextArea.click(); + await this.page.mouse.wheel(0, -100000000); + await expect(this.editorTextArea).toContainText(match); + } + + // TODO: Rename to expectProjectFiles + async findProjectFiles(expected: string[]): Promise { + await this.switchTab("Project"); + await expect(this.page.getByRole("listitem")).toHaveText(expected); } - /** - * Close the page, accepting any native dialogs (e.g. beforeunload). - * - * @returns a boolean representing whether a "beforeunload" dialog is raised. - */ - async closePageCheckDialog(): Promise { - const page = await this.page; - await page.close({ - runBeforeUnload: true, - }); - await waitFor(() => { - expect(page.isClosed()).toEqual(true); - }, defaultWaitForOptions); - return this.dialogs.length === 1 && this.dialogs[0] === "beforeunload"; - } - - /** - * Reload the page, accepting any native dialogs (e.g. beforeunload). - */ - async reloadPage(): Promise { - const page = await this.page; - await page.reload(); - } - - /** - * Open a file using the file chooser. - * - * @param filePath The file on disk. - * @param options Options to control expectations after upload. - */ async loadFiles( - filePath: string, + filePathFromProjectRoot: string, options: { acceptDialog?: LoadDialogType } = {} - ): Promise { + ) { await this.switchTab("Project"); - const document = await this.document(); - const openInput = (await document.getAllByTestId( - "open-input" - )) as ElementHandle[]; - await openInput[0].uploadFile(filePath); + await this.projectTab.chooseFile(filePathFromProjectRoot); + if (options.acceptDialog !== undefined) { - await this.findAndAcceptLoadDialog(options.acceptDialog); + const loadDialog = new LoadDialog(this.page, options.acceptDialog); + await loadDialog.submit(); } } - /** - * Add a new file using the files tab. - * - * @param name The name to enter in the dialog. - */ - async createNewFile(name: string): Promise { - await this.switchTab("Project"); - const document = await this.document(); - const createFileButton = await document.findByRole("button", { - name: "Create file", - }); - await createFileButton.click(); - const nameField = await document.findByRole("textbox", { - name: "Name", - }); - await nameField.type(name); - const createButton = await document.findByRole("button", { - name: "Create", - }); - await createButton.click(); - } - - /** - * Open a file using drag and drop. - * - * This is a bit fragile and likely to break if we change the DnD DOM as - * we resort to simulating DnD events. - * - * @param filePath The file on disk. - */ async dropFile( - filePath: string, + filePathFromProjectRoot: string, options: { acceptDialog?: LoadDialogType } = {} - ): Promise { - const page = await this.page; - // Puppeteer doesn't have file drop support but we can use an input - // to grab a file and trigger an event that's good enough. - // It's a bit of a pain as the drop happens on an element created by - // the drag-over. - // https://github.com/puppeteer/puppeteer/issues/1376 - // This issue has since been fixed and we've upgraded, so there's an opportunity to simplify here. - const inputId = "simulated-drop-input"; - await page.evaluate((inputId) => { - const input = document.createElement("input"); - input.style.display = "none"; - input.type = "file"; - input.id = inputId; - input.onchange = (e: any) => { - const dragOverZone = document.querySelector( - "[data-testid=project-drop-target]" - ); - if (!dragOverZone) { - throw new Error(); - } - const dragOverEvent = new Event("dragover", { - bubbles: true, - }); - const dropEvent = new Event("drop", { - bubbles: true, - }); - (dragOverEvent as any).dataTransfer = { types: ["Files"] }; - (dropEvent as any).dataTransfer = { files: e.target.files }; - dragOverZone.dispatchEvent(dragOverEvent); - - const dropWhenReady = () => { - const dropZone = document.querySelector( - "[data-testid=project-drop-target-overlay]" - ); - if (dropZone) { - dropZone!.dispatchEvent(dropEvent); - input.remove(); - } else { - setTimeout(dropWhenReady, 10); - } - }; - - dropWhenReady(); - }; - document.body.appendChild(input); - }, inputId); - const fileInput = (await page.$( - `#${inputId}` - )) as ElementHandle; - await fileInput.uploadFile(filePath); + ) { + const filePath = getAbsoluteFilePath(filePathFromProjectRoot); + const filename = getFilename(filePathFromProjectRoot); + + // wait for page to load + await this.saveButton.waitFor(); + + // Playwright drag and drop file method taken from + // https://github.com/microsoft/playwright/issues/10667#issuecomment-998397241 + const buffer = readFileSync(filePath, { encoding: "ascii" }); + const dataTransfer = await this.page.evaluateHandle( + ({ buffer, filename }) => { + const dt = new DataTransfer(); + const file = new File([buffer], filename); + dt.items.add(file); + return dt; + }, + { buffer, filename } + ); + + // Drag file over target area to reveal drop zone + await this.page + .getByTestId("project-drop-target") + .dispatchEvent("dragover", { dataTransfer }); + + const dropZone = this.page.getByTestId("project-drop-target-overlay"); + await dropZone.waitFor(); + await dropZone.dispatchEvent("drop", { dataTransfer }); + if (options.acceptDialog !== undefined) { - await this.findAndAcceptLoadDialog(options.acceptDialog); + const loadDialog = new LoadDialog(this.page, options.acceptDialog); + await loadDialog.submit(); } } - private async findAndAcceptLoadDialog(dialogType: LoadDialogType) { - if (dialogType === LoadDialogType.CONFIRM) { - return this.findAndClickButton("Confirm"); - } - if (dialogType === LoadDialogType.REPLACE) { - return this.findAndClickButton("Replace"); - } - if (dialogType === LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE) { - // Use the Option menu to change how we load the file. - await this.findAndClickButton("Options"); - const document = await this.document(); - const menuItem = await document.findByText(/^(Add|Replace) file .+\.py$/); - await menuItem.click(); - - return this.findAndClickButton("Confirm"); + // TODO: Rename to expectAlertText + async findAlertText(title: string, description?: string): Promise { + await expect(this.page.getByText(title)).toBeVisible(); + if (description) { + await expect(this.page.getByText(description)).toBeVisible(); } } - private async findAndClickButton(name: string): Promise { - const document = await this.document(); - const button = await document.findByRole("button", { - name: name, - }); - await button.click(); + async isDeleteFileOptionDisabled(filename: string) { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + return await fileOptionMenu.deleteButton.isDisabled(); } - async switchLanguage(locale: string): Promise { - // All test ids so they can be language invariant. - const document = await this.document(); - await this.clickSettingsMenu(); - await (await document.findByTestId("language")).click(); - await (await document.findByTestId(locale)).click(); + async isEditFileOptionDisabled(filename: string) { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + return await fileOptionMenu.editButton.isDisabled(); } - private async clickSettingsMenu(): Promise { - // All test ids for the sake of language-related tests. - const document = await this.document(); - return (await document.findByTestId("settings")).click(); + // TODO: Rename to editFile + async switchToEditing(filename: string): Promise { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + await fileOptionMenu.editButton.click(); } async findThirdPartyModuleWarning( expectedName: string, expectedVersion: string ): Promise { - const document = await this.document(); - await Promise.all([ - document.findByText(expectedName), - document.findByText(expectedVersion), - ]); + for (const name in [expectedName, expectedVersion]) { + await expect(this.page.getByRole("cell", { name })).toBeVisible(); + } } - async toggleSettingThirdPartyModuleEditing(): Promise { - await this.clickSettingsMenu(); - const document = await this.document(); - const settings = await document.findByRole("menuitem", { - name: "Settings", - }); - await settings.click(); - const checkbox = await document.findByRole("checkbox", { - name: "Allow editing third-party modules", - }); - // Regular click() doesn't work here. - await checkbox.evaluate((e) => (e as any).click()); - await this.findAndClickButton("Close"); + async closeDialog(dialogText?: string) { + if (dialogText) { + await this.page.getByText(dialogText).waitFor(); + } + await this.page.getByRole("button", { name: "Close" }).first().click(); } - /** - * Use the Files sidebar to change the current file we're editing. - * - * @param filename The name of the file in the file list. - */ - async switchToEditing(filename: string): Promise { - await this.openFileActionsMenu(filename); - const document = await this.document(); - const editButton = await document.findByRole("menuitem", { - name: "Edit " + filename, - }); - await editButton.click(); - } - - /** - * Can switch to editing a file. - * - * For now we only support editing Python files. - * - * @param filename The name of the file in the file list. - */ - async canSwitchToEditing(filename: string): Promise { - await this.openFileActionsMenu(filename); - const document = await this.document(); - const editButton = await document.findByRole("menuitem", { - name: "Edit " + filename, - }); - return !(await isDisabled(editButton)); + async save(options: SaveOptions = { waitForDownload: true }) { + if (!options.waitForDownload) { + await this.saveButton.click(); + return; + } + const downloadPromise = this.page.waitForEvent("download"); + await this.saveButton.click(); + return await downloadPromise; } - /** - * Uses the Files tab to delete a file. - * - * @param filename The filename. - */ - async deleteFile( - filename: string, - dialogChoice: string = "Delete" - ): Promise { - await this.openFileActionsMenu(filename); - const document = await this.document(); - const button = await document.findByRole("menuitem", { - name: "Delete " + filename, - }); - await button.click(); - const dialogButton = await document.findByRole("button", { - name: dialogChoice, - }); - await dialogButton.click(); + // TODO: Rename to savePythonScript + async saveMain() { + await this.page.getByTestId("more-save-options").click(); + const downloadPromise = this.page.waitForEvent("download"); + await this.page + .getByRole("menuitem", { name: "Save Python script" }) + .click(); + await downloadPromise; } - async canDeleteFile(filename: string): Promise { - await this.openFileActionsMenu(filename); - const document = await this.document(); - const button = await document.findByRole("menuitem", { - name: `Delete ${filename}`, - }); - - return !(await isDisabled(button)); + // TODO: Rename to expectDialog + async confirmInputDialog(text: string) { + await expect(this.page.getByText(text)).toBeVisible(); } - /** - * Wait for an alert, throwing if it doesn't happen. - * - * @param title The expected alert title. - * @param description The expected alert description (if any). - */ - async findAlertText(title: string, description?: string): Promise { - const document = await this.document(); - // role=status queries don't work by content - const titles = await document.findAllByText(title); - if (description) { - for (const title of titles) { - const parentElement = (await title.getProperty( - "parentElement" - )) as ElementHandle; - const descriptionMatch = await parentElement.getByText(description); - if (descriptionMatch) { - return; - } - throw new Error("Not found!"); - } - } + async deleteFile(filename: string) { + await this.switchTab("Project"); + const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); + await fileOptionMenu.delete(); } - /** - * Wait for the editor contents to match the given regexp, throwing if it doesn't happen. - * - * Only the first few lines will be visible. - * - * @param match The regex. - */ - async findVisibleEditorContents( - match: RegExp | string, - options?: waitForOptions - ): Promise { - if (typeof match === "string") { - match = new RegExp(escapeRegExp(match)); - } - const document = await this.document(); - let lastText: string | undefined; - const text = () => - document.evaluate(() => { - // We use the testid to identify the main editor as we also have read-only code views. - const lines = Array.from( - window.document.querySelectorAll("[data-testid='editor'] .cm-line") - ); - return ( - lines - .map((l) => (l as HTMLElement).innerText) - // Blank lines here are \n but non-blank lines have no trailing separator. Fix so we can join the text. - .map((l) => (l === "\n" ? "" : l)) - .join("\n") - ); - }); - return waitFor( - async () => { - const value = await text(); - lastText = value; - expect(value).toMatch(match); - }, - { - ...defaultWaitForOptions, - onTimeout: (_e) => - new Error( - `Timeout waiting for ${match} but content was:\n${lastText}}\n\nJSON version:\n${JSON.stringify( - lastText - )}` - ), - ...options, - } - ); + async toggleSettingThirdPartyModuleEditing(): Promise { + await this.settingsButton.click(); + await this.page.getByRole("menuitem", { name: "Settings" }).click(); + await this.page + .getByText("Allow editing third-party modules", { exact: true }) + .click(); + await this.page.getByRole("button", { name: "Close" }).click(); } - /** - * Type in the editor area. - * - * This will focus the editor area and type with the caret in its default position - * (the beginning unless we've otherwise interacted with it). - * - * @param text The text to type. - */ - async typeInEditor(text: string): Promise { - const content = await this.focusEditorContent(); - // The short delay seems to improve reliability triggering autocomplete. - // Previously finding autocomplete options failed approx 1 in 30 times. - // https://github.com/microbit-foundation/python-editor-v3/issues/419 - return content.type(text, { delay: 10 }); - } - - /** - * Select all the text in the code editor. - * - * Subsequent typing will overwrite it. - */ - async selectAllInEditor(): Promise { - await this.focusEditorContent(); - const keyboard = (await this.page).keyboard; - const meta = process.platform === "darwin" ? "Meta" : "Control"; - await keyboard.down(meta); - await keyboard.press("a"); - await keyboard.up(meta); - } - - /** - * Edit the project name. - * - * @param projectName The new name. - */ - async setProjectName(projectName: string): Promise { - const document = await this.document(); - const editButton = await document.findByRole( - "button", - { - name: "Edit project name", - }, - defaultWaitForOptions - ); - await editButton.click(); - const input = await document.findByRole("textbox", { - name: /Name/, + // Rename to closeAndExpectBeforeUnloadDialogVisible + async closePageCheckDialog(visible: boolean): Promise { + this.page.on("dialog", async (dialog) => { + expect(dialog.type() === "beforeunload").toEqual(visible); + await dialog.dismiss(); }); - await input.type(projectName); - const confirm = await document.findByRole("button", { name: "Confirm" }); - await confirm.click(); - } - - /** - * Wait for the project name - * - * @param match - * @returns - */ - async findProjectName(match: string): Promise { - const text = async () => { - const document = await this.document(); - const projectName = await document.getByTestId("project-name"); - return projectName.getNodeText(); - }; - return waitFor(async () => { - const value = await text(); - expect(value).toEqual(match); - }, defaultWaitForOptions); - } - - async findActiveApiEntry(text: string, headingLevel: string): Promise { - // We need to make sure it's actually visible as it's scroll-based navigation. - const document = await this.document(); - return waitFor(async () => { - const items = await document.$$(headingLevel); - const headings: { text: string; visible: boolean }[] = await Promise.all( - items.map((e: ElementHandle) => - e.evaluate((node) => { - const text = (node as HTMLElement).innerText; - const rect = (node as HTMLElement).getBoundingClientRect(); - const visible = rect.top >= 0 && rect.bottom <= window.innerHeight; - return { text, visible }; - }) - ) - ); - const match = headings.find((info) => info.visible && info.text === text); - expect(match).toBeDefined(); - }, defaultWaitForOptions); + this.page.close({ runBeforeUnload: true }); } + // Rename to expectDocumentationTopLevelHeading async findDocumentationTopLevelHeading( title: string, description?: string ): Promise { - const document = await this.document(); - await document.findByText( - title, - { - selector: "h2", - }, - defaultWaitForOptions - ); + await expect( + this.page.getByRole("heading", { name: title, exact: true }) + ).toBeVisible(); if (description) { - await document.findByText(description); + await expect(this.page.getByText(description)).toBeVisible(); } } async selectDocumentationSection(name: string): Promise { - const document = await this.document(); - const button = await document.findByRole("button", { - name: `View ${name} documentation`, - }); - await button.click(); - await this.awaitAnimation(); + await this.page.getByRole("heading", { name }).click(); } - async awaitAnimation(): Promise { - const page = await this.page; - await page.waitForTimeout(300); + async toggleCodeActionButton(name: string): Promise { + await this.page + .getByRole("listitem") + .filter({ hasText: name }) + .getByRole("button", { name: "More" }) + .click(); } - async selectDocumentationIdea(name: string): Promise { - const document = await this.document(); - const heading = await document.findByText(name, { - selector: "h3", - }); - const handle = heading.asElement(); - await handle!.evaluate((element) => { - element.parentElement?.click(); - }); + async selectToolkitDropDownOption( + label: string, + option: string + ): Promise { + await this.page.getByRole("combobox", { name: label }).selectOption(option); } - async insertToolkitCode(name: string): Promise { - const document = await this.document(); - const heading = await document.findByText(name, { - selector: "h3", - }); - const handle = heading.asElement(); - await handle!.evaluate((element) => { - const item = element.closest("li"); - item!.querySelector("button")!.click(); - }); + private getCodeExample(name: string) { + return this.page + .getByRole("listitem") + .filter({ hasText: name }) + .locator("div") + .filter({ + hasText: "Code example:", + }) + .nth(2); } - async triggerScroll(tabName: string): Promise { - const document = await this.document(); - const button = await document.findByRole("button", { - name: tabName, - }); - const handle = button.asElement(); - await handle!.evaluate((element) => { - const scrollablePanel = element.closest( - "[data-testid='scrollable-panel']" - ); - scrollablePanel?.scrollTo({ top: 10 }); - }); - await this.awaitAnimation(); + async copyCode(name: string) { + await this.getCodeExample(name).click(); + await this.page.getByRole("button", { name: "Copy code" }).click(); } - async toggleCodeActionButton(name: string): Promise { - const document = await this.document(); - const heading = await document.findByText(name, { - selector: "h3", - }); - const handle = heading.asElement() as ElementHandle; - await handle.evaluate((element) => { - const item = element.closest("li"); - (item!.querySelector(".cm-content") as HTMLButtonElement)!.click(); - }); - } + async dragDropCodeEmbed(name: string, targetLine: number) { + const codeExample = this.getCodeExample(name); + const editorLine = this.page + .getByTestId("editor") + .getByRole("textbox") + .locator("div") + .filter({ hasText: targetLine.toString() }); - async copyCode(): Promise { - const document = await this.document(); - const copyCodeButton = await document.findByRole("button", { - name: "Copy code", - }); - await copyCodeButton.click(); + await codeExample.dragTo(editorLine); } - async pasteToolkitCode(): Promise { - await this.focusEditorContent(); - const keyboard = (await this.page).keyboard; - const meta = process.platform === "darwin" ? "Meta" : "Control"; + // TODO: Rename to search + async searchToolkits(searchText: string): Promise { + await this.switchTab("Reference"); + await this.searchButton.click(); + await this.page.getByPlaceholder("Search").fill(searchText); + } - // With the current version of Pupepteer this doesn't seem to work on Macs - // On upgrading we can fix like this: https://github.com/puppeteer/puppeteer/pull/9357/files - await keyboard.down(meta); - await keyboard.press("v"); - await keyboard.up(meta); + async selectFirstSearchResult(): Promise { + // wait for results to show + await this.page.getByRole("link").first().waitFor(); + const links = await this.page.getByRole("link").all(); + await links[0].click(); } - async selectToolkitDropDownOption( - label: string, - option: string - ): Promise { - const document = await this.document(); - const select = await document.findByLabelText(label); - await select.select(option); - } - - /** - * Trigger a save but don't wait for it to complete. - * - * Useful when the action is expected to fail. - * Otherwise see waitForSave. - */ - async save(): Promise { - const document = await this.document(); - const saveButton = await document.findByText("Save"); - return saveButton.click(); - } - - async saveMain(): Promise { - const document = await this.document(); - const moreSaveOptions = await document.findByTestId("more-save-options"); - await moreSaveOptions.click(); - const saveMainButton = await document.findByRole("menuitem", { - name: "Save Python script", - }); - await saveMainButton.click(); + async selectDocumentationIdea(name: string): Promise { + await this.page.getByRole("button", { name }).click(); } async connect(): Promise { - const document = await this.document(); - const moreConnectOptions = await document.findByTestId( - "more-connect-options" - ); - await moreConnectOptions.click(); - const connectButton = await document.findByRole("menuitem", { - name: "Connect", - }); - await connectButton.click(); + await this.moreConnectionOptionsButton.click(); + await this.page.getByRole("menuitem", { name: "Connect" }).click(); await this.connectViaConnectHelp(); } // Connects from the connect dialog/wizard. async connectViaConnectHelp(): Promise { - const document = await this.document(); - const nextButtonOne = await document.findByRole("button", { - name: "Next", - }); - await nextButtonOne.click(); - const nextButtonTwo = await document.findByRole("button", { - name: "Next", - }); - await nextButtonTwo.click(); - } - - async confirmConnection(): Promise { - const serialArea = await this.findMainSerialArea(); - await serialArea.findByRole("button", { - name: "Serial menu", - }); - } - - async confirmGenericDialog(title: string): Promise { - const document = await this.document(); - await document.findByText(title, { - selector: "h2", - }); + await this.page.getByRole("button", { name: "Next" }).click(); + await this.page.getByRole("button", { name: "Next" }).click(); } - async confirmInputDialog(title: string): Promise { - const document = await this.document(); - await document.findByText(title, { - selector: "h2", - }); - } - - // Launch 'connect help' dialog from 'not found' dialog. - async connectHelpFromNotFoundDialog(): Promise { - const document = await this.document(); - const reviewDeviceSelection = await document.findByRole("link", { - name: "follow these steps", - }); - await reviewDeviceSelection.click(); - } - - async closeDialog(title?: string): Promise { - const document = await this.document(); - if (title) { - await document.findByText(title, { - selector: "h2", - }); - } - // This finds the "X" button in the top right of the dialog - // and the footer button. - const closeButton = await document.findAllByRole("button", { - name: "Close", + // TODO: Extract as variable instead of function + private findMainSerialArea() { + return this.page.getByRole("region", { + name: "Serial terminal", + exact: true, }); - await closeButton[0].click(); } - // Retry micro:bit connection from error dialogs. - async connectViaTryAgain(): Promise { - const document = await this.document(); - const tryAgainButton = await document.findByRole("button", { - name: "Try again", + async confirmConnection(): Promise { + const serialMenu = this.findMainSerialArea().getByRole("button", { + name: "Serial menu", }); - await tryAgainButton.click(); + await expect(serialMenu).toBeVisible(); } + // TODO: Move expect out to separate function async disconnect(): Promise { - const document = await this.document(); - const moreConnectOptions = await document.findByTestId( - "more-connect-options" - ); - await moreConnectOptions.click(); - const disconnectButton = await document.findByRole("menuitem", { - name: "Disconnect", - }); - await disconnectButton.click(); - return waitFor( - async () => { - expect( - ( - await document.queryAllByRole("button", { - name: "Serial terminal", - }) - ).length - ).toEqual(0); - }, - { - ...defaultWaitForOptions, - onTimeout: () => new Error("Serial still present after disconnect"), - } - ); - } + await this.moreConnectionOptionsButton.click(); + await this.page.getByRole("menuitem", { name: "Disconnect" }).click(); + const btns = await this.page + .getByRole("button", { name: "Serial terminal" }) + .all(); - private async findMainSerialArea() { - const document = await this.document(); - return document.findByRole("region", { - name: "Serial terminal", - }); + expect(btns.length).toEqual(0); } async serialShow(): Promise { - const mainSerialArea = await this.findMainSerialArea(); - const showSerialButton = await mainSerialArea.findByRole("button", { - name: "Show serial", - }); - await showSerialButton.click(); + await this.findMainSerialArea() + .getByRole("button", { name: "Show serial" }) + .click(); + + // TODO: Extract // Make sure the button has flipped. - await mainSerialArea.findByRole("button", { + const hideSerialButton = this.findMainSerialArea().getByRole("button", { name: "Hide serial", }); + await expect(hideSerialButton).toBeVisible(); } async serialHide(): Promise { - const serialArea = await this.findMainSerialArea(); - const hideSerialButton = await serialArea.findByRole("button", { - name: "Hide serial", - }); - await hideSerialButton.click(); + await this.findMainSerialArea() + .getByRole("button", { name: "Hide serial" }) + .click(); + + // TODO: Extract // Make sure the button has flipped. - await serialArea.findByRole("button", { + const showSerialButton = this.findMainSerialArea().getByRole("button", { name: "Show serial", }); - } - - async findSerialCompactTraceback(text: Matcher): Promise { - const document = await this.document(); - await document.findByText(text); + await expect(showSerialButton).toBeVisible(); } async flash() { - const document = await this.document(); - const flash = await document.findByRole("button", { - name: "Send to micro:bit", - }); - return flash.click(); - } - - async followSerialCompactTracebackLink(): Promise { - const document = await this.document(); - const link = await document.findByTestId("traceback-link"); - await link.click(); + await this.page.getByRole("button", { name: "Send to micro:bit" }).click(); } async mockSerialWrite(data: string): Promise { - const page = await this.page; - page.evaluate((data) => { + this.page.evaluate((data) => { (window as any).mockDevice.mockSerialWrite(data); }, toCrLf(data)); } + async followSerialCompactTracebackLink(): Promise { + await this.page.getByTestId("traceback-link").click(); + } + async mockDeviceConnectFailure(code: WebUSBErrorCode) { - const page = await this.page; - page.evaluate((code) => { + this.page.evaluate((code) => { (window as any).mockDevice.mockConnect(code); }, code); } + async findSerialCompactTraceback(text: string | RegExp): Promise { + await expect(this.page.getByText(text)).toBeVisible(); + } + + // Retry micro:bit connection from error dialogs. + async connectViaTryAgain(): Promise { + await this.page.getByRole("button", { name: "Try again" }).click(); + } + + // Launch 'connect help' dialog from 'not found' dialog. + async connectHelpFromNotFoundDialog(): Promise { + await this.page.getByRole("link", { name: "follow these steps" }).click(); + } + async mockWebUsbNotSupported() { - const page = await this.page; - page.evaluate(() => { + this.page.evaluate(() => { (window as any).mockDevice.mockWebUsbNotSupported(); }); } - /** - * Trigger a hex file save and wait for the download to complete. - * - * @returns Download details. - */ - async waitForSave(): Promise { - return this.waitForDownloadOnDisk(() => this.save()); + // TODO: Rename to expectCompletionOptions + // try toContainText instead for testing! + async findCompletionOptions(expected: string[]): Promise { + const completions = this.page.getByRole("listbox", { name: "Completions" }); + const contents = await completions.innerText(); + + expect(contents).toEqual(expected.join("\n")); } - /** - * Resets the page for a new test. - */ - async reset() { - let page = await this.page; - if (!page.isClosed()) { - page.removeAllListeners(); - await page.close(); - } - this.page = this.createPage(); - page = await this.page; - await page.goto(this.url); - // Wait for side bar to load - await page.waitForSelector('[data-testid="scrollable-panel"]'); - } - - /** - * Navigate to the URL defined by options. - * - * Only needed to test initialization scenarios when options has been - * changed by the test. - */ - async gotoOptionsUrl() { - let page = await this.page; - // Allow testing fragment changes by actually navigating away. - await page.goto("about:blank"); - return page.goto(this.url); - } - - /** - * Wait for matching completion options to appear. - */ - async findCompletionOptions(expected: string[]): Promise { - const document = await this.document(); - return waitFor(async () => { - const items: ElementHandle[] = await document.$$( - ".cm-completionLabel" - ); - const actual = await Promise.all( - items.map((e) => e.evaluate((node) => (node as HTMLElement).innerText)) - ); - expect(actual).toEqual(expected); - }, defaultWaitForOptions); - } - - /** - * Wait for the a signature tooltip to appear with a matching signature. - */ - async findSignatureHelp(expectedSignature: string): Promise { - const document = await this.document(); - return waitFor(async () => { - const tooltip = await document.$(".cm-signature-tooltip code"); - expect(tooltip).toBeTruthy(); - const actualSignature = await tooltip!.evaluate( - (e) => (e as HTMLElement).innerText - ); - expect(actualSignature).toEqual(expectedSignature); - }, defaultWaitForOptions); - } - - /** - * Wait for active completion option by waiting for its signature to be shown - * in the documentation tooltip area. - */ + // TODO: Rename to expectCompletionActiveOption async findCompletionActiveOption(signature: string): Promise { - const document = await this.document(); - await document.findByText( - signature, - { - selector: "code", - }, - defaultWaitForOptions - ); + const activeOption = this.editor + .locator("div") + .filter({ hasText: signature }) + .nth(2); + await expect(activeOption).toBeVisible(); } - /** - * Accept the given completion. - */ async acceptCompletion(name: string): Promise { // This seems significantly more reliable than pressing Enter, though there's // no real-life issue here. - const document = await this.document(); - const editor = await document.findByTestId("editor"); - const option = await editor.findByRole( - "option", - { - name, - }, - defaultWaitForOptions - ); - await option.click(); + await this.editor.getByRole("option", { name }).click(); } - /** - * Follow the documentation link shown in the signature help or autocomplete tooltips. - * This will update the "API" tab and switch to it. - */ async followCompletionOrSignatureDocumentionLink( - linkName: string + linkName: "Help" | "API" ): Promise { - const document = await this.document(); - const button = await document.findByRole("link", { - name: linkName, - }); - await button.click(); - - // Wait for side bar to load - await document.waitForSelector('[data-testid="scrollable-panel"]'); + await this.page.getByRole("link", { name: linkName }).click(); } - /** - * Drag the first code snippet from the named section to the target line. - * The section must alread be in view. - * - * @param name The name of the section. - * @param targetLine The target line (1-based). - */ - async dragDropCodeEmbed(name: string, targetLine: number) { - const page = await this.page; - const document = await this.document(); - const heading = await document.findByRole("heading", { - name, - level: 3, - }); - const section: puppeteer.ElementHandle = - await heading.evaluateHandle((e: Element) => { - let node: Element | null = e; - while (node && node.tagName !== "LI") { - node = node.parentElement; - } - if (!node) { - throw new Error("Unexpected DOM structure"); - } - return node; - }); - const draggable = (await section.$("[draggable]"))!; - const lines = await document.$$("[data-testid='editor'] .cm-line"); - const line = lines[targetLine - 1]; - if (!line) { - throw new Error(`No line ${targetLine} found. Line must exist.`); - } - await page.setDragInterception(true); - await draggable.dragAndDrop(line); - } - - async resetProject(): Promise { - await this.switchTab("Project"); - await this.findAndClickButton("Reset project"); - await this.findAndClickButton("Replace"); - } - - async findProjectFiles(expected: string[]): Promise { - const tab = await this.switchTab("Project"); - const items = async () => { - const items = await tab.findAllByRole("listitem"); - const text = await Promise.all( - items.map((i) => i.evaluate((e) => e.textContent)) - ); - return text; - }; - return waitFor(async () => { - const actual = await items(); - expect(actual).toEqual(expected); - }, defaultWaitForOptions); - } - - /** - * Take a screenshot named after the running test case and store it in the reports folder. - * The folder is published in CI. - */ - async screenshot() { - const page = await this.page; - return page.screenshot({ - path: this.reportFilename("png"), - }); + // TODO: Rename to expectActiveApiEntry + async findActiveApiEntry(text: string, _headingLevel: string): Promise { + // We need to make sure it's actually visible as it's scroll-based navigation. + await expect(this.page.getByRole("heading", { name: text })).toBeVisible(); } - private reportFilename(extension: string): string { - return ( - reportsPath + - // GH actions has character restrictions - (expect.getState().currentTestName || "").replace(/[^0-9a-zA-Z]+/g, "-") + - "." + - extension - ); + // TODO: Rename to expectSignatureHelp + async findSignatureHelp(expectedSignature: string): Promise { + const signatureHelp = this.editor + .locator("div") + .filter({ hasText: expectedSignature }) + .nth(1); + await signatureHelp.waitFor(); + await expect(signatureHelp).toBeVisible(); } - private async focusEditorContent(): Promise { - const document = await this.document(); - const editor = await document.findByTestId( - "editor", - {}, - defaultWaitForOptions - ); - const content = await editor.$(".cm-content"); - if (!content) { - throw new Error("Missing editor area"); + // Simulator functions + private getSimulatorIframe(): Frame { + const simulatorIframe = this.page + .frames() + .find((frame) => frame.name() === "Simulator"); + if (!simulatorIframe) { + throw new Error("Simulator iframe not found"); } - await content.focus(); - return content; - } - - /** - * Clean up, including the browser and downloads temporary folder. - */ - async dispose() { - await fsp.rm(this.downloadPath, { recursive: true }); - const page = await this.page; - await page.browser().close(); - } - - /** - * Switch to a sidebar tab. - * - * Prefer more specific navigation actions, but this is useful to check initial state - * and that tab state is remembered. - */ - async switchTab( - tabName: "Project" | "API" | "Reference" | "Ideas" - ): Promise> { - const document = await this.document(); - const tab = await document.findByRole( - "tab", - { - name: tabName, - }, - defaultWaitForOptions - ); - await tab.click(); - return document.findByRole("tabpanel"); - } - - async searchToolkits(searchText: string): Promise { - const document = await this.document(); - const searchButton = await document.findByRole("button", { - name: "Search", - }); - await searchButton.click(); - const searchField = await document.findByRole("textbox", { - name: "Search", - }); - await searchField.type(searchText); + return simulatorIframe; } - async selectFirstSearchResult(): Promise { - const document = await this.document(); - const modalDialog = await document.findByRole("dialog"); - const result = await modalDialog.findAllByRole( - "heading", - { - level: 3, - }, - defaultWaitForOptions - ); - await result[0].click(); + async runSimulator(): Promise { + const simulatorIframe = this.getSimulatorIframe(); + const playButton = simulatorIframe.locator(".play-button"); + await playButton.click(); } - async tabOutOfEditorForwards(): Promise { - const content = await this.focusEditorContent(); - await content.press("Escape"); - await content.press("Tab"); + async simulatorSelectGesture(option: string): Promise { + await this.page + .getByTestId("simulator-gesture-select") + .selectOption(option); } - async tabOutOfEditorBackwards(): Promise { - const keyboard = (await this.page).keyboard; - - const content = await this.focusEditorContent(); - await content.press("Escape"); - await keyboard.down("Shift"); - await content.press("Tab"); - await keyboard.up("Shift"); - } - - private async document(): Promise> { - const page = await this.page; - return page.getDocument(); - } - - private async waitForDownloadOnDisk( - triggerDownload: () => Promise, - timeout: number = 5000 - ): Promise { - const listDir = async () => { - const listing = await fsp.readdir(this.downloadPath); - return new Set(listing.filter((x) => !x.endsWith(".crdownload"))); - }; - - const before = await listDir(); - await triggerDownload(); - - const startTime = performance.now(); - // eslint-disable-next-line no-constant-condition - while (true) { - const after = await listDir(); - before.forEach((x) => after.delete(x)); - if (after.size === 1) { - const filename = after.values().next().value; - const data = await fsp.readFile(path.join(this.downloadPath, filename)); - return { filename, data }; - } - if (after.size > 1) { - throw new Error("Unexpected extra file in downloads directory"); - } - if (performance.now() - startTime > timeout) { - throw new Error("Timeout waiting for puppeteer download"); - } - await new Promise((resolve) => setTimeout(resolve, 20)); - } + async simulatorSendGesture(): Promise { + await this.page.getByRole("button", { name: "Send gesture" }).click(); } - private async openFileActionsMenu(filename: string): Promise { - await this.switchTab("Project"); - const document = await this.document(); - const actions = await document.findByRole("button", { - name: `${filename} file actions`, - }); - await actions.click(); + async simulatorConfirmResponse(): Promise { + // Confirms that top left LED is switched on + // to match Image.NO being displayed. + const gridLEDs = this.getSimulatorIframe().locator("#LEDsOn"); + await expect(gridLEDs).toBeVisible(); } - private async keyboardPress(key: KeyInput): Promise { - const keyboard = (await this.page).keyboard; - await keyboard.press(key); + async simulatorSetRangeSlider( + sliderLabel: string, + value: "min" | "max" + ): Promise { + const sliderThumb = this.page.locator( + `[role="slider"][aria-label="${sliderLabel}"]` + ); + const bounding_box = await sliderThumb!.boundingBox(); + await this.page.mouse.move( + bounding_box!.x + bounding_box!.width / 2, + bounding_box!.y + bounding_box!.height / 2 + ); + await this.page.mouse.down(); + await this.page.waitForTimeout(500); + await this.page.mouse.move(value === "max" ? 1200 : 0, 0); + await this.page.waitForTimeout(500); + await this.page.mouse.up(); } - private async getElementByQuerySelector( - query: string - ): Promise> { - const document = await this.document(); - const result = await document.$(query); - if (!result) { - throw new Error(); - } - return result; + async simulatorInputPressHold( + name: string, + pressDuration: number + ): Promise { + const inputButton = this.page.getByRole("button", { + name, + }); + const bounding_box = await inputButton!.boundingBox(); + await this.page.mouse.move( + bounding_box!.x + bounding_box!.width / 2, + bounding_box!.y + bounding_box!.height / 2 + ); + await this.page.mouse.down(); + await this.page.waitForTimeout(pressDuration); + await this.page.mouse.up(); } - async assertActiveElement( - accessExpectedElement: () => Promise> - ) { - return waitFor(async () => { - const page = await this.page; - const expectedElement = await accessExpectedElement(); - - expect( - await page.evaluate((e) => { - return e === document.activeElement; - }, expectedElement) - ).toEqual(true); - }, defaultWaitForOptions); + async findStoppedSimulator(): Promise { + const button = this.page.getByRole("button", { + name: "Stop simulator", + }); + expect(await button.isDisabled()).toEqual(true); } + // TODO: Rename to expectFocusOnLoad async assertFocusOnLoad(): Promise { - const document = await this.document(); - // Do this first so we know it's ready to be tabbed to. - const link = await document.findByRole("link", { - name: "visit microbit.org (opens in a new tab)", - }); - await this.keyboardPress("Tab"); - return this.assertActiveElement(() => Promise.resolve(link)); + const link = this.page.getByLabel( + "visit microbit.org (opens in a new tab)" + ); + await this.page.keyboard.press("Tab"); + await expect(link).toBeFocused(); } - collapseSimulator(): Promise { - return this.findAndClickButton("Collapse simulator"); + async collapseSimulator(): Promise { + await this.simulator.collapseButton.click(); } - expandSimulator(): Promise { - return this.findAndClickButton("Expand simulator"); + async expandSimulator(): Promise { + await this.simulator.expandButton.click(); } - collapseSidebar(): Promise { - return this.findAndClickButton("Collapse sidebar"); + async collapseSidebar(): Promise { + await this.sidebar.collapseButton.click(); } - expandSidebar(): Promise { - return this.findAndClickButton("Expand sidebar"); + async expandSidebar(): Promise { + await this.sidebar.expandButton.click(); } async assertFocusOnExpandSimulator(): Promise { - const document = await this.document(); - return this.assertActiveElement(() => - document.getByRole("button", { name: "Expand simulator" }) - ); + await expect(this.simulator.expandButton).toBeFocused(); } - assertFocusOnSimulator(): Promise { - return this.assertActiveElement(() => - this.getElementByQuerySelector("iframe[name='Simulator']") - ); + async assertFocusOnSimulator(): Promise { + const simulator = this.page.locator("iframe[name='Simulator']"); + await expect(simulator).toBeFocused(); } async assertFocusOnExpandSidebar(): Promise { - const document = await this.document(); - return this.assertActiveElement(() => - document.findByRole("button", { name: "Expand sidebar" }) - ); + await expect(this.sidebar.expandButton).toBeFocused(); } - assertFocusOnSidebar(): Promise { - return this.assertActiveElement(() => - this.getElementByQuerySelector("[role='tabpanel']") - ); + async assertFocusOnSidebar(): Promise { + const simulator = this.page.getByRole("tabpanel", { name: "Reference" }); + await expect(simulator).toBeFocused(); } async assertFocusBeforeEditor(): Promise { - const document = await this.document(); - return this.assertActiveElement(() => - document.findByRole("button", { - name: "Zoom in", - }) - ); + const zoomIn = this.page.getByRole("button", { + name: "Zoom in", + }); + await expect(zoomIn).toBeFocused(); } async assertFocusAfterEditor(): Promise { - const document = await this.document(); - return this.assertActiveElement(() => - document.findByRole("button", { - name: "Send to micro:bit", - }) - ); - } - - // Simulator functions - private async getSimulatorIframe(): Promise { - const page = await this.page; - const simulatorIframe = page - .frames() - .find((frame) => frame.name() === "Simulator"); - if (!simulatorIframe) { - throw new Error("Simulator iframe not found"); - } - return simulatorIframe; - } - - async runSimulator(): Promise { - const simulatorIframe = await this.getSimulatorIframe(); - const playButton = await simulatorIframe!.$(".play-button"); - await playButton!.click(); - } - - async findStoppedSimulator(): Promise { - const document = await this.document(); - const stopButton = await document.findByRole("button", { - name: "Stop simulator", + const sendButton = this.page.getByRole("button", { + name: "Send to micro:bit", }); - waitFor(async () => { - expect(await isDisabled(stopButton)).toEqual(true); - }, defaultWaitForOptions); - } - - async simulatorSelectGesture(option: string): Promise { - const document = await this.document(); - const select = await document.findByTestId("simulator-gesture-select"); - await select.select(option); + await expect(sendButton).toBeFocused(); } - async simulatorSendGesture(): Promise { - const document = await this.document(); - const gestureSendBtn = await document.getByRole("button", { - name: "Send gesture", - }); - await gestureSendBtn.click(); + async tabOutOfEditorForwards(): Promise { + await this.editor.click(); + await this.page.keyboard.press("Escape"); + await this.page.keyboard.press("Tab"); } - async simulatorInputPressHold( - name: string, - pressDuration: number - ): Promise { - const page = await this.page; - const document = await this.document(); - const inputButton = await document.getByRole("button", { - name, - }); - const bounding_box = await inputButton!.boundingBox(); - await page.mouse.move( - bounding_box!.x + bounding_box!.width / 2, - bounding_box!.y + bounding_box!.height / 2 - ); - await page.mouse.down(); - await page.waitForTimeout(pressDuration); - await page.mouse.up(); + async tabOutOfEditorBackwards(): Promise { + await this.editor.click(); + await this.page.keyboard.press("Escape"); + await this.page.keyboard.down("Shift"); + await this.page.keyboard.press("Tab"); + await this.page.keyboard.up("Shift"); } +} - async simulatorSetRangeSlider( - sliderLabel: string, - value: "min" | "max" - ): Promise { - const page = await this.page; - const document = await this.document(); - const sliderThumb = await document.waitForSelector( - `[role="slider"][aria-label="${sliderLabel}"]` - ); - const bounding_box = await sliderThumb!.boundingBox(); - await page.mouse.move( - bounding_box!.x + bounding_box!.width / 2, - bounding_box!.y + bounding_box!.height / 2 - ); - await page.mouse.down(); - await page.waitForTimeout(500); - await page.mouse.move(value === "max" ? 1200 : 0, 0); - await page.waitForTimeout(500); - await page.mouse.up(); - } +const toCrLf = (text: string): string => + text.replace(/[\r\n]/g, "\n").replace(/\n/g, "\r\n"); - async simulatorConfirmResponse(): Promise { - // Confirms that top left LED is switched on - // to match Image.NO being displayed. - const simulatorIframe = await this.getSimulatorIframe(); - const gridLEDs = await simulatorIframe!.$("#LEDsOn"); - await gridLEDs!.waitForSelector("use", { - visible: true, - timeout: defaultWaitForOptions.timeout, - }); +export const getFilename = (filePath: string) => { + const filename = filePath.split("/").pop(); + if (!filename) { + throw new Error("dropFile Error: No filename found!"); } -} + return filename; +}; -/** - * Checks whether an element is disabled. - * - * @param element an element handle. - * @returns true if the element exists and is marked disabled. - */ -const isDisabled = async (element: ElementHandle) => { - if (!element) { - return false; - } - const disabled = await element.getProperty("disabled"); - return disabled && (await disabled.jsonValue()); +const getAbsoluteFilePath = (filePathFromProjectRoot: string) => { + const dir = path.dirname(fileURLToPath(import.meta.url)); + return path.join(dir.replace("src/e2e", ""), filePathFromProjectRoot); }; -const toCrLf = (text: string): string => - text.replace(/[\r\n]/g, "\n").replace(/\n/g, "\r\n"); +const optionsToURL = (options: UrlOptions): string => { + const flags = new Set([ + "none", + "noWelcome", + ...(options.flags ?? []), + ]); + const params: Array<[string, string]> = Array.from(flags).map((f) => [ + "flag", + f, + ]); + if (options.language) { + params.push(["l", options.language]); + } + return ( + baseUrl + + // We didn't use BASE_URL here as CRA seems to set it to "" before running jest. + // Maybe can be changed since the Vite upgrade. + (process.env.E2E_BASE_URL ?? "/") + + "?" + + new URLSearchParams(params) + + (options.fragment ?? "") + ); +}; diff --git a/src/e2e/multiple-files.test.ts b/src/e2e/multiple-files.test.ts index 95c4352e3..e958c2456 100644 --- a/src/e2e/multiple-files.test.ts +++ b/src/e2e/multiple-files.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ import { expect } from "@playwright/test"; -import { LoadDialogType } from "./app-playwright.js"; +import { LoadDialogType } from "./app.js"; import { test } from "./app-test-fixtures.js"; test.describe("multiple-files", () => { diff --git a/src/e2e/open.test.ts b/src/e2e/open.test.ts index 1bae75916..9bf909779 100644 --- a/src/e2e/open.test.ts +++ b/src/e2e/open.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ import { expect } from "@playwright/test"; -import { LoadDialogType } from "./app-playwright.js"; +import { LoadDialogType } from "./app.js"; import { test } from "./app-test-fixtures.js"; test.describe("open", () => { diff --git a/src/e2e/save.test.ts b/src/e2e/save.test.ts index f683c441d..a41e7adf7 100644 --- a/src/e2e/save.test.ts +++ b/src/e2e/save.test.ts @@ -5,7 +5,7 @@ */ import { expect } from "@playwright/test"; import fs from "fs"; -import { LoadDialogType } from "./app-playwright.js"; +import { LoadDialogType } from "./app.js"; import { test } from "./app-test-fixtures.js"; test.describe("save", () => { From 499e1b27476673983026f038f1e72032c6618b25 Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 18 Mar 2024 17:06:35 +0000 Subject: [PATCH 19/38] Use `npm run serve` to serve editor --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 5b8972be6..442a90160 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -24,7 +24,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "npm run start", + command: "npm run serve", url: "http://127.0.0.1:3000", reuseExistingServer: !process.env.CI, }, From cadbe540d68287f0f0eaaa70768e398c14dd093a Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 18 Mar 2024 17:09:30 +0000 Subject: [PATCH 20/38] Configure webServer for CI --- playwright.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 442a90160..321389be7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -24,8 +24,15 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "npm run serve", - url: "http://127.0.0.1:3000", + ...(process.env.CI + ? { + command: `npx vite preview --port 3000 --base ${process.env.BASE_URL}`, + url: `http://localhost:3000${process.env.BASE_URL}`, + } + : { + command: "npm run serve", + url: "http://localhost:3000/", + }), reuseExistingServer: !process.env.CI, }, }); From 373dd28428660309767c7faa61c3a04ee2d267d4 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 09:01:59 +0000 Subject: [PATCH 21/38] Upgrade playwright to 1.33 That is the version where running vite as web server doesn't cause hanging. We can now revert back to using vite for running webserver. --- package-lock.json | 16 ++++++++-------- package.json | 2 +- playwright.config.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3a33c434..7508e30a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "1.32.0", + "@playwright/test": "1.33.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", @@ -2837,13 +2837,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz", - "integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.33.0.tgz", + "integrity": "sha512-YunBa2mE7Hq4CfPkGzQRK916a4tuZoVx/EpLjeWlTVOnD4S2+fdaQZE0LJkbfhN5FTSKNLdcl7MoT5XB37bTkg==", "dev": true, "dependencies": { "@types/node": "*", - "playwright-core": "1.32.0" + "playwright-core": "1.33.0" }, "bin": { "playwright": "cli.js" @@ -8906,9 +8906,9 @@ } }, "node_modules/playwright-core": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz", - "integrity": "sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.33.0.tgz", + "integrity": "sha512-aizyPE1Cj62vAECdph1iaMILpT0WUDCq3E6rW6I+dleSbBoGbktvJtzS6VHkZ4DKNEOG9qJpiom/ZxO+S15LAw==", "dev": true, "bin": { "playwright": "cli.js" diff --git a/package.json b/package.json index ad2c5524c..089de5836 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "1.32.0", + "@playwright/test": "1.33.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", diff --git a/playwright.config.ts b/playwright.config.ts index 321389be7..52ddff47b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,8 +30,8 @@ export default defineConfig({ url: `http://localhost:3000${process.env.BASE_URL}`, } : { - command: "npm run serve", - url: "http://localhost:3000/", + command: "npm run start", + url: "http://localhost:3000", }), reuseExistingServer: !process.env.CI, }, From 32519c5aa3365f82cc019dd36fc7960d9f7daeda Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 10:33:40 +0000 Subject: [PATCH 22/38] Refactoring --- src/e2e/accessibility.test.ts | 17 +- src/e2e/app.ts | 311 +++++++++++++-------------------- src/e2e/autocomplete.test.ts | 26 +-- src/e2e/connect.test.ts | 36 ++-- src/e2e/documentation.test.ts | 10 +- src/e2e/edits.test.ts | 6 +- src/e2e/multiple-files.test.ts | 10 +- src/e2e/open.test.ts | 16 +- src/e2e/reset.test.ts | 2 +- src/e2e/save.test.ts | 10 +- src/e2e/simulator.test.ts | 26 +-- 11 files changed, 204 insertions(+), 266 deletions(-) diff --git a/src/e2e/accessibility.test.ts b/src/e2e/accessibility.test.ts index 0f2c01891..77247b7eb 100644 --- a/src/e2e/accessibility.test.ts +++ b/src/e2e/accessibility.test.ts @@ -4,30 +4,31 @@ * SPDX-License-Identifier: MIT */ import { test } from "./app-test-fixtures.js"; +import { expect } from "@playwright/test"; test.describe("accessibility", () => { test("focuses the correct element on tabbing after load", async ({ app }) => { - await app.assertFocusOnLoad(); + await app.expectFocusOnLoad(); }); test("focuses the correct elements on collapsing and expanding the simulator", async ({ app, }) => { - await app.collapseSimulator(); - await app.assertFocusOnExpandSimulator(); + await app.simulator.collapseButton.click(); + await expect(app.simulator.expandButton).toBeFocused(); - await app.expandSimulator(); - await app.assertFocusOnSimulator(); + await app.simulator.expandButton.click(); + await expect(app.simulator.iframe).toBeFocused(); }); test("focuses the correct elements on collapsing and expanding the sidebar", async ({ app, }) => { - await app.expandSidebar(); + await app.sidebar.expandButton.click(); await app.assertFocusOnSidebar(); - await app.collapseSidebar(); - await app.assertFocusOnExpandSidebar(); + await app.sidebar.collapseButton.click(); + await expect(app.sidebar.expandButton).toBeFocused(); }); test("allows tab out of editor", async ({ app }) => { diff --git a/src/e2e/app.ts b/src/e2e/app.ts index eb4294346..a7f85ea39 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -129,10 +129,103 @@ class SideBar { class Simulator { public expandButton: Locator; public collapseButton: Locator; + public showSerialButton: Locator; + public hideSerialButton: Locator; + public sendGestureButton: Locator; + private stopButton: Locator; + public serialMenu: Locator; + public iframe: Locator; + private serialArea: Locator; constructor(public readonly page: Page) { this.expandButton = this.page.getByLabel("Expand simulator"); this.collapseButton = this.page.getByLabel("Collapse simulator"); + + this.serialArea = this.page.getByRole("region", { + name: "Serial terminal", + exact: true, + }); + this.serialMenu = this.getSerialAreaButton("Serial menu"); + this.showSerialButton = this.getSerialAreaButton("Show serial"); + this.hideSerialButton = this.getSerialAreaButton("Hide serial"); + this.sendGestureButton = this.page.getByRole("button", { + name: "Send gesture", + }); + this.stopButton = this.page.getByRole("button", { + name: "Stop simulator", + }); + this.iframe = this.page.locator("iframe[name='Simulator']"); + } + + private getSerialAreaButton(name: string) { + return this.serialArea.getByRole("button", { name }); + } + + async simulatorSelectGesture(option: string): Promise { + await this.page + .getByTestId("simulator-gesture-select") + .selectOption(option); + } + + // Simulator functions + private getSimulatorIframe(): Frame { + const simulatorIframe = this.page + .frames() + .find((frame) => frame.name() === "Simulator"); + if (!simulatorIframe) { + throw new Error("Simulator iframe not found"); + } + return simulatorIframe; + } + + async run(): Promise { + const simulatorIframe = this.getSimulatorIframe(); + const playButton = simulatorIframe.locator(".play-button"); + await playButton.click(); + } + + async expectResponse(): Promise { + // Confirms that top left LED is switched on + // to match Image.NO being displayed. + const gridLEDs = this.getSimulatorIframe().locator("#LEDsOn"); + await expect(gridLEDs).toBeVisible(); + } + + async expectStopped(): Promise { + expect(await this.stopButton.isDisabled()).toEqual(true); + } + + async setRangeSlider( + sliderLabel: string, + value: "min" | "max" + ): Promise { + const sliderThumb = this.page.locator( + `[role="slider"][aria-label="${sliderLabel}"]` + ); + const bounding_box = await sliderThumb!.boundingBox(); + await this.page.mouse.move( + bounding_box!.x + bounding_box!.width / 2, + bounding_box!.y + bounding_box!.height / 2 + ); + await this.page.mouse.down(); + await this.page.waitForTimeout(500); + await this.page.mouse.move(value === "max" ? 1200 : 0, 0); + await this.page.waitForTimeout(500); + await this.page.mouse.up(); + } + + async inputPressHold(name: string, pressDuration: number): Promise { + const inputButton = this.page.getByRole("button", { + name, + }); + const bounding_box = await inputButton!.boundingBox(); + await this.page.mouse.move( + bounding_box!.x + bounding_box!.width / 2, + bounding_box!.y + bounding_box!.height / 2 + ); + await this.page.mouse.down(); + await this.page.waitForTimeout(pressDuration); + await this.page.mouse.up(); } } @@ -148,6 +241,7 @@ export class App { private editor: Locator; public simulator: Simulator; public sidebar: SideBar; + public sendToMicrobitButton: Locator; constructor(public readonly page: Page, public context: BrowserContext) { this.baseUrl = baseUrl; @@ -163,6 +257,9 @@ export class App { this.moreConnectionOptionsButton = this.page.getByTestId( "more-connect-options" ); + this.sendToMicrobitButton = this.page.getByRole("button", { + name: "Send to micro:bit", + }); this.simulator = new Simulator(this.page); this.sidebar = new SideBar(this.page); @@ -199,7 +296,6 @@ export class App { await this.page.keyboard.press(`${this.modifierKey}+A`); } - // TODO: Rename to pasteInEditor async pasteInEditor() { // Simulating keyboard press CTRL+V works in Playwright, // but does not work in this case potentially due to @@ -239,7 +335,6 @@ export class App { await this.page.getByRole("button", { name: "Replace" }).click(); } - // Use allInnerTexts() for matching text async expectEditorContainText(match: RegExp | string) { // Scroll to the top of code text area await this.editorTextArea.click(); @@ -247,8 +342,7 @@ export class App { await expect(this.editorTextArea).toContainText(match); } - // TODO: Rename to expectProjectFiles - async findProjectFiles(expected: string[]): Promise { + async expectProjectFiles(expected: string[]): Promise { await this.switchTab("Project"); await expect(this.page.getByRole("listitem")).toHaveText(expected); } @@ -304,8 +398,7 @@ export class App { } } - // TODO: Rename to expectAlertText - async findAlertText(title: string, description?: string): Promise { + async expectAlertText(title: string, description?: string): Promise { await expect(this.page.getByText(title)).toBeVisible(); if (description) { await expect(this.page.getByText(description)).toBeVisible(); @@ -324,8 +417,7 @@ export class App { return await fileOptionMenu.editButton.isDisabled(); } - // TODO: Rename to editFile - async switchToEditing(filename: string): Promise { + async editFile(filename: string): Promise { await this.switchTab("Project"); const fileOptionMenu = await this.projectTab.openFileActionsMenu(filename); await fileOptionMenu.editButton.click(); @@ -357,8 +449,7 @@ export class App { return await downloadPromise; } - // TODO: Rename to savePythonScript - async saveMain() { + async savePythonScript() { await this.page.getByTestId("more-save-options").click(); const downloadPromise = this.page.waitForEvent("download"); await this.page @@ -367,8 +458,7 @@ export class App { await downloadPromise; } - // TODO: Rename to expectDialog - async confirmInputDialog(text: string) { + async expectDialog(text: string) { await expect(this.page.getByText(text)).toBeVisible(); } @@ -387,8 +477,9 @@ export class App { await this.page.getByRole("button", { name: "Close" }).click(); } - // Rename to closeAndExpectBeforeUnloadDialogVisible - async closePageCheckDialog(visible: boolean): Promise { + async closeAndExpectBeforeUnloadDialogVisible( + visible: boolean + ): Promise { this.page.on("dialog", async (dialog) => { expect(dialog.type() === "beforeunload").toEqual(visible); await dialog.dismiss(); @@ -396,8 +487,7 @@ export class App { this.page.close({ runBeforeUnload: true }); } - // Rename to expectDocumentationTopLevelHeading - async findDocumentationTopLevelHeading( + async expectDocumentationTopLevelHeading( title: string, description?: string ): Promise { @@ -455,8 +545,7 @@ export class App { await codeExample.dragTo(editorLine); } - // TODO: Rename to search - async searchToolkits(searchText: string): Promise { + async search(searchText: string): Promise { await this.switchTab("Reference"); await this.searchButton.click(); await this.page.getByPlaceholder("Search").fill(searchText); @@ -479,68 +568,28 @@ export class App { await this.connectViaConnectHelp(); } + async disconnect(): Promise { + await this.moreConnectionOptionsButton.click(); + await this.page.getByRole("menuitem", { name: "Disconnect" }).click(); + } + // Connects from the connect dialog/wizard. async connectViaConnectHelp(): Promise { await this.page.getByRole("button", { name: "Next" }).click(); await this.page.getByRole("button", { name: "Next" }).click(); } - // TODO: Extract as variable instead of function - private findMainSerialArea() { - return this.page.getByRole("region", { - name: "Serial terminal", - exact: true, - }); + async expectConnected(): Promise { + await expect(this.simulator.serialMenu).toBeVisible(); } - async confirmConnection(): Promise { - const serialMenu = this.findMainSerialArea().getByRole("button", { - name: "Serial menu", - }); - await expect(serialMenu).toBeVisible(); - } - - // TODO: Move expect out to separate function - async disconnect(): Promise { - await this.moreConnectionOptionsButton.click(); - await this.page.getByRole("menuitem", { name: "Disconnect" }).click(); + async expectDisconnected(): Promise { const btns = await this.page .getByRole("button", { name: "Serial terminal" }) .all(); - expect(btns.length).toEqual(0); } - async serialShow(): Promise { - await this.findMainSerialArea() - .getByRole("button", { name: "Show serial" }) - .click(); - - // TODO: Extract - // Make sure the button has flipped. - const hideSerialButton = this.findMainSerialArea().getByRole("button", { - name: "Hide serial", - }); - await expect(hideSerialButton).toBeVisible(); - } - - async serialHide(): Promise { - await this.findMainSerialArea() - .getByRole("button", { name: "Hide serial" }) - .click(); - - // TODO: Extract - // Make sure the button has flipped. - const showSerialButton = this.findMainSerialArea().getByRole("button", { - name: "Show serial", - }); - await expect(showSerialButton).toBeVisible(); - } - - async flash() { - await this.page.getByRole("button", { name: "Send to micro:bit" }).click(); - } - async mockSerialWrite(data: string): Promise { this.page.evaluate((data) => { (window as any).mockDevice.mockSerialWrite(data); @@ -557,7 +606,7 @@ export class App { }, code); } - async findSerialCompactTraceback(text: string | RegExp): Promise { + async expectSerialCompactTraceback(text: string | RegExp): Promise { await expect(this.page.getByText(text)).toBeVisible(); } @@ -577,17 +626,13 @@ export class App { }); } - // TODO: Rename to expectCompletionOptions - // try toContainText instead for testing! - async findCompletionOptions(expected: string[]): Promise { + async expectCompletionOptions(expected: string[]): Promise { const completions = this.page.getByRole("listbox", { name: "Completions" }); const contents = await completions.innerText(); - expect(contents).toEqual(expected.join("\n")); } - // TODO: Rename to expectCompletionActiveOption - async findCompletionActiveOption(signature: string): Promise { + async expectCompletionActiveOption(signature: string): Promise { const activeOption = this.editor .locator("div") .filter({ hasText: signature }) @@ -607,14 +652,12 @@ export class App { await this.page.getByRole("link", { name: linkName }).click(); } - // TODO: Rename to expectActiveApiEntry - async findActiveApiEntry(text: string, _headingLevel: string): Promise { + async expectActiveApiEntry(text: string): Promise { // We need to make sure it's actually visible as it's scroll-based navigation. await expect(this.page.getByRole("heading", { name: text })).toBeVisible(); } - // TODO: Rename to expectSignatureHelp - async findSignatureHelp(expectedSignature: string): Promise { + async expectSignatureHelp(expectedSignature: string): Promise { const signatureHelp = this.editor .locator("div") .filter({ hasText: expectedSignature }) @@ -623,85 +666,7 @@ export class App { await expect(signatureHelp).toBeVisible(); } - // Simulator functions - private getSimulatorIframe(): Frame { - const simulatorIframe = this.page - .frames() - .find((frame) => frame.name() === "Simulator"); - if (!simulatorIframe) { - throw new Error("Simulator iframe not found"); - } - return simulatorIframe; - } - - async runSimulator(): Promise { - const simulatorIframe = this.getSimulatorIframe(); - const playButton = simulatorIframe.locator(".play-button"); - await playButton.click(); - } - - async simulatorSelectGesture(option: string): Promise { - await this.page - .getByTestId("simulator-gesture-select") - .selectOption(option); - } - - async simulatorSendGesture(): Promise { - await this.page.getByRole("button", { name: "Send gesture" }).click(); - } - - async simulatorConfirmResponse(): Promise { - // Confirms that top left LED is switched on - // to match Image.NO being displayed. - const gridLEDs = this.getSimulatorIframe().locator("#LEDsOn"); - await expect(gridLEDs).toBeVisible(); - } - - async simulatorSetRangeSlider( - sliderLabel: string, - value: "min" | "max" - ): Promise { - const sliderThumb = this.page.locator( - `[role="slider"][aria-label="${sliderLabel}"]` - ); - const bounding_box = await sliderThumb!.boundingBox(); - await this.page.mouse.move( - bounding_box!.x + bounding_box!.width / 2, - bounding_box!.y + bounding_box!.height / 2 - ); - await this.page.mouse.down(); - await this.page.waitForTimeout(500); - await this.page.mouse.move(value === "max" ? 1200 : 0, 0); - await this.page.waitForTimeout(500); - await this.page.mouse.up(); - } - - async simulatorInputPressHold( - name: string, - pressDuration: number - ): Promise { - const inputButton = this.page.getByRole("button", { - name, - }); - const bounding_box = await inputButton!.boundingBox(); - await this.page.mouse.move( - bounding_box!.x + bounding_box!.width / 2, - bounding_box!.y + bounding_box!.height / 2 - ); - await this.page.mouse.down(); - await this.page.waitForTimeout(pressDuration); - await this.page.mouse.up(); - } - - async findStoppedSimulator(): Promise { - const button = this.page.getByRole("button", { - name: "Stop simulator", - }); - expect(await button.isDisabled()).toEqual(true); - } - - // TODO: Rename to expectFocusOnLoad - async assertFocusOnLoad(): Promise { + async expectFocusOnLoad(): Promise { const link = this.page.getByLabel( "visit microbit.org (opens in a new tab)" ); @@ -709,35 +674,6 @@ export class App { await expect(link).toBeFocused(); } - async collapseSimulator(): Promise { - await this.simulator.collapseButton.click(); - } - - async expandSimulator(): Promise { - await this.simulator.expandButton.click(); - } - - async collapseSidebar(): Promise { - await this.sidebar.collapseButton.click(); - } - - async expandSidebar(): Promise { - await this.sidebar.expandButton.click(); - } - - async assertFocusOnExpandSimulator(): Promise { - await expect(this.simulator.expandButton).toBeFocused(); - } - - async assertFocusOnSimulator(): Promise { - const simulator = this.page.locator("iframe[name='Simulator']"); - await expect(simulator).toBeFocused(); - } - - async assertFocusOnExpandSidebar(): Promise { - await expect(this.sidebar.expandButton).toBeFocused(); - } - async assertFocusOnSidebar(): Promise { const simulator = this.page.getByRole("tabpanel", { name: "Reference" }); await expect(simulator).toBeFocused(); @@ -751,10 +687,7 @@ export class App { } async assertFocusAfterEditor(): Promise { - const sendButton = this.page.getByRole("button", { - name: "Send to micro:bit", - }); - await expect(sendButton).toBeFocused(); + await expect(this.sendToMicrobitButton).toBeFocused(); } async tabOutOfEditorForwards(): Promise { diff --git a/src/e2e/autocomplete.test.ts b/src/e2e/autocomplete.test.ts index 81965a7ac..9ae8219bf 100644 --- a/src/e2e/autocomplete.test.ts +++ b/src/e2e/autocomplete.test.ts @@ -14,12 +14,12 @@ test.describe("autocomplete", () => { await app.typeInEditor("from microbit import *\ndisplay.s"); // Initial completions - await app.findCompletionOptions(["scroll", "set_pixel", "show"]); - await app.findCompletionActiveOption("scroll(text)"); + await app.expectCompletionOptions(["scroll", "set_pixel", "show"]); + await app.expectCompletionActiveOption("scroll(text)"); // Further refinement await app.page.keyboard.press("h"); - await app.findCompletionActiveOption("show(image)"); + await app.expectCompletionActiveOption("show(image)"); // Accepted completion await app.acceptCompletion("show"); @@ -33,18 +33,18 @@ test.describe("autocomplete", () => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.show(image"); - await app.findCompletionOptions(["Image", "image="]); + await app.expectCompletionOptions(["Image", "image="]); }); test("autocomplete can navigate to API toolkit content", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.sho"); - await app.findCompletionActiveOption("show(image)"); + await app.expectCompletionActiveOption("show(image)"); await app.followCompletionOrSignatureDocumentionLink("API"); - await app.findActiveApiEntry(showFullSignature, "h4"); + await app.expectActiveApiEntry(showFullSignature); }); test("autocomplete can navigate to Reference toolkit content", async ({ @@ -52,9 +52,9 @@ test.describe("autocomplete", () => { }) => { await app.selectAllInEditor(); await app.typeInEditor("from microbit import *\ndisplay.sho"); - await app.findCompletionActiveOption("show(image)"); + await app.expectCompletionActiveOption("show(image)"); await app.followCompletionOrSignatureDocumentionLink("Help"); - await app.findActiveApiEntry("Show", "h3"); + await app.expectActiveApiEntry("Show"); }); test("shows signature help after autocomplete", async ({ app }) => { @@ -62,7 +62,7 @@ test.describe("autocomplete", () => { await app.typeInEditor("from microbit import *\ndisplay.sho"); await app.acceptCompletion("show"); - await app.findSignatureHelp(showFullSignature); + await app.expectSignatureHelp(showFullSignature); }); test("does not insert brackets for import completion", async ({ app }) => { @@ -79,11 +79,11 @@ test.describe("autocomplete", () => { // The closing bracket is autoinserted. await app.typeInEditor("from microbit import *\ndisplay.show("); - await app.findSignatureHelp(showFullSignature); + await app.expectSignatureHelp(showFullSignature); await app.followCompletionOrSignatureDocumentionLink("API"); - await app.findActiveApiEntry(showFullSignature, "h4"); + await app.expectActiveApiEntry(showFullSignature); }); test("signature can navigate to Reference toolkit content", async ({ @@ -92,8 +92,8 @@ test.describe("autocomplete", () => { await app.selectAllInEditor(); // The closing bracket is autoinserted. await app.typeInEditor("from microbit import *\ndisplay.show("); - await app.findSignatureHelp(showFullSignature); + await app.expectSignatureHelp(showFullSignature); await app.followCompletionOrSignatureDocumentionLink("Help"); - await app.findActiveApiEntry("Show", "h3"); + await app.expectActiveApiEntry("Show"); }); }); diff --git a/src/e2e/connect.test.ts b/src/e2e/connect.test.ts index d407fde00..56ed52746 100644 --- a/src/e2e/connect.test.ts +++ b/src/e2e/connect.test.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: MIT */ import { test } from "./app-test-fixtures.js"; +import { expect } from "@playwright/test"; const traceback = `Traceback (most recent call last): File "main.py", line 6 @@ -14,29 +15,32 @@ test.describe("connect", () => { test("shows serial when connected", async ({ app }) => { // Connect and disconnect wait for serial to be shown/hidden await app.connect(); - await app.confirmConnection(); + await app.expectConnected(); await app.disconnect(); + await app.expectDisconnected(); }); - test("can expand serial to show full output", async ({ app }) => { + test("can expand/collapse serial", async ({ app }) => { await app.connect(); - await app.serialShow(); + await app.simulator.showSerialButton.click(); + await expect(app.simulator.hideSerialButton).toBeVisible(); - await app.serialHide(); + await app.simulator.hideSerialButton.click(); + await expect(app.simulator.showSerialButton).toBeVisible(); }); test("shows summary of traceback from serial", async ({ app }) => { await app.connect(); - await app.flash(); + await app.sendToMicrobitButton.click(); await app.mockSerialWrite(traceback); - await app.findSerialCompactTraceback(/SyntaxError: invalid syntax/); + await app.expectSerialCompactTraceback(/SyntaxError: invalid syntax/); }); test("supports navigating to line from traceback", async ({ app }) => { await app.connect(); - await app.flash(); + await app.sendToMicrobitButton.click(); await app.mockSerialWrite(traceback); await app.followSerialCompactTracebackLink(); @@ -49,10 +53,10 @@ test.describe("connect", () => { }) => { await app.mockDeviceConnectFailure("no-device-selected"); await app.connect(); - await app.confirmInputDialog("No micro:bit found"); + await app.expectDialog("No micro:bit found"); await app.connectViaTryAgain(); await app.connectViaConnectHelp(); - await app.confirmConnection(); + await app.expectConnected(); }); test("shows the micro:bit not found dialog and connects after launching the connect help dialog", async ({ @@ -60,10 +64,10 @@ test.describe("connect", () => { }) => { await app.mockDeviceConnectFailure("no-device-selected"); await app.connect(); - await app.confirmInputDialog("No micro:bit found"); + await app.expectDialog("No micro:bit found"); await app.connectHelpFromNotFoundDialog(); await app.connectViaConnectHelp(); - await app.confirmConnection(); + await app.expectConnected(); }); test("shows the update firmware dialog and connects on try again", async ({ @@ -71,10 +75,10 @@ test.describe("connect", () => { }) => { await app.mockDeviceConnectFailure("update-req"); await app.connect(); - await app.confirmInputDialog("Firmware update required"); + await app.expectDialog("Firmware update required"); await app.connectViaTryAgain(); await app.connectViaConnectHelp(); - await app.confirmConnection(); + await app.expectConnected(); }); test("Shows the transfer hex help dialog after send to micro:bit where WebUSB is not supported", async ({ @@ -82,9 +86,9 @@ test.describe("connect", () => { }) => { await app.mockWebUsbNotSupported(); await app.setProjectName("not default name"); - await app.flash(); - await app.confirmInputDialog("This browser does not support WebUSB"); + await app.sendToMicrobitButton.click(); + await app.expectDialog("This browser does not support WebUSB"); await app.closeDialog(); - await app.confirmInputDialog("Transfer saved hex file to micro:bit"); + await app.expectDialog("Transfer saved hex file to micro:bit"); }); }); diff --git a/src/e2e/documentation.test.ts b/src/e2e/documentation.test.ts index e90e6c5ed..7a63976a6 100644 --- a/src/e2e/documentation.test.ts +++ b/src/e2e/documentation.test.ts @@ -8,7 +8,7 @@ import { test } from "./app-test-fixtures.js"; test.describe("documentation", () => { test("API toolkit navigation", async ({ app }) => { await app.switchTab("API"); - await app.findDocumentationTopLevelHeading( + await app.expectDocumentationTopLevelHeading( "API", "For usage and examples, see" ); @@ -61,9 +61,9 @@ test.describe("documentation", () => { }); test("Searches and navigates to the first result", async ({ app }) => { - await app.searchToolkits("loop"); + await app.search("loop"); await app.selectFirstSearchResult(); - await app.findDocumentationTopLevelHeading( + await app.expectDocumentationTopLevelHeading( "Loops", "Count and repeat sets of instructions" ); @@ -71,7 +71,7 @@ test.describe("documentation", () => { test("Ideas tab navigation", async ({ app }) => { await app.switchTab("Ideas"); - await app.findDocumentationTopLevelHeading( + await app.expectDocumentationTopLevelHeading( "Ideas", "Try out these projects, modify them and get inspired" ); @@ -81,6 +81,6 @@ test.describe("documentation", () => { const ideaName = "Emotion badge"; await app.switchTab("Ideas"); await app.selectDocumentationIdea(ideaName); - await app.findDocumentationTopLevelHeading(ideaName); + await app.expectDocumentationTopLevelHeading(ideaName); }); }); diff --git a/src/e2e/edits.test.ts b/src/e2e/edits.test.ts index f5f036bf0..356dcf528 100644 --- a/src/e2e/edits.test.ts +++ b/src/e2e/edits.test.ts @@ -7,14 +7,14 @@ import { test } from "./app-test-fixtures.js"; test.describe("edits", () => { test("doesn't prompt on close if no edits made", async ({ app }) => { - await app.closePageCheckDialog(false); + await app.closeAndExpectBeforeUnloadDialogVisible(false); }); test("prompts on close if file edited", async ({ app }) => { await app.typeInEditor("A change!"); await app.expectEditorContainText(/A change/); - await app.closePageCheckDialog(true); + await app.closeAndExpectBeforeUnloadDialogVisible(true); }); test("prompts on close if project name edited", async ({ app }) => { @@ -22,7 +22,7 @@ test.describe("edits", () => { await app.setProjectName(name); await app.expectProjectName(name); - await app.closePageCheckDialog(true); + await app.closeAndExpectBeforeUnloadDialogVisible(true); }); test("retains text across a reload via session storage", async ({ app }) => { diff --git a/src/e2e/multiple-files.test.ts b/src/e2e/multiple-files.test.ts index e958c2456..d5dc69df0 100644 --- a/src/e2e/multiple-files.test.ts +++ b/src/e2e/multiple-files.test.ts @@ -12,7 +12,7 @@ test.describe("multiple-files", () => { // Probably best for this to be an error or else we // need to cope with no Python at all to display. await app.loadFiles("src/micropython/main/microbit-micropython-v2.hex"); - await app.findAlertText( + await app.expectAlertText( "Cannot load file", "No appended code found in the hex file" ); @@ -22,7 +22,7 @@ test.describe("multiple-files", () => { await app.createNewFile("test"); await app.expectEditorContainText(/Your new file/); - await app.findProjectFiles(["main.py", "test.py"]); + await app.expectProjectFiles(["main.py", "test.py"]); }); test("Prevents deleting main.py", async ({ app }) => { @@ -33,7 +33,7 @@ test.describe("multiple-files", () => { await app.loadFiles("testData/usermodule.py", { acceptDialog: LoadDialogType.CONFIRM_BUT_LOAD_AS_MODULE, }); - await app.switchToEditing("usermodule.py"); + await app.editFile("usermodule.py"); await app.expectEditorContainText(/b_works/); await app.loadFiles("testData/updated/usermodule.py", { @@ -47,7 +47,7 @@ test.describe("multiple-files", () => { await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); - await app.switchToEditing("module.py"); + await app.editFile("module.py"); await app.findThirdPartyModuleWarning("a", "1.0.0"); await app.toggleSettingThirdPartyModuleEditing(); @@ -67,7 +67,7 @@ test.describe("multiple-files", () => { await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); - await app.switchToEditing("module.py"); + await app.editFile("module.py"); await app.deleteFile("module.py"); diff --git a/src/e2e/open.test.ts b/src/e2e/open.test.ts index 9bf909779..fb55d8b10 100644 --- a/src/e2e/open.test.ts +++ b/src/e2e/open.test.ts @@ -11,7 +11,7 @@ test.describe("open", () => { test("Shows an alert when loading a MakeCode hex", async ({ app }) => { await app.loadFiles("testData/makecode.hex"); - await app.findAlertText( + await app.expectAlertText( "Cannot load file", "This hex file cannot be loaded in the Python Editor. The Python Editor cannot open hex files created with Microsoft MakeCode." ); @@ -22,7 +22,7 @@ test.describe("open", () => { acceptDialog: LoadDialogType.CONFIRM, }); - await app.findAlertText("Updated file main.py"); + await app.expectAlertText("Updated file main.py"); await app.expectProjectName("Untitled project"); }); @@ -31,7 +31,7 @@ test.describe("open", () => { acceptDialog: LoadDialogType.NONE, }); - await app.findAlertText( + await app.expectAlertText( "Cannot load file", // Would be great to have custom messages here but needs error codes // pushing into microbit-fs. @@ -65,7 +65,7 @@ test.describe("open", () => { acceptDialog: LoadDialogType.NONE, }); - await app.findAlertText( + await app.expectAlertText( "Cannot load file", "This version of the Python Editor doesn't currently support adding .mpy files." ); @@ -90,12 +90,12 @@ test.describe("open", () => { acceptDialog: LoadDialogType.CONFIRM, }); - await app.findAlertText("Added file module.py"); + await app.expectAlertText("Added file module.py"); await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); - await app.findAlertText("Updated file module.py"); + await app.expectAlertText("Updated file module.py"); }); test("Warns before load if you have changes", async ({ app }) => { @@ -121,7 +121,7 @@ test.describe("open", () => { test("No warn before load if you save main file", async ({ app }) => { await app.setProjectName("Avoid dialog"); await app.typeInEditor("# Different text"); - await app.saveMain(); + await app.savePythonScript(); // No dialog accepted await app.loadFiles("testData/1.0.1.hex"); @@ -134,7 +134,7 @@ test.describe("open", () => { await app.setProjectName("Avoid dialog"); await app.typeInEditor("# Different text"); await app.createNewFile("another"); - await app.saveMain(); + await app.savePythonScript(); await app.closeDialog("Warning: Only main.py downloaded"); await app.loadFiles("testData/1.0.1.hex", { diff --git a/src/e2e/reset.test.ts b/src/e2e/reset.test.ts index 106bc02a2..2ccfd9cea 100644 --- a/src/e2e/reset.test.ts +++ b/src/e2e/reset.test.ts @@ -17,6 +17,6 @@ test.describe("reset", () => { // Everything's back to normal. await app.expectProjectName("Untitled project"); await app.expectEditorContainText("from microbit import"); - await app.findProjectFiles(["main.py"]); + await app.expectProjectFiles(["main.py"]); }); }); diff --git a/src/e2e/save.test.ts b/src/e2e/save.test.ts index a41e7adf7..cf2b3d074 100644 --- a/src/e2e/save.test.ts +++ b/src/e2e/save.test.ts @@ -37,7 +37,7 @@ test.describe("save", () => { await app.expectEditorContainText(/# Filler/); await app.save({ waitForDownload: false }); - await app.findAlertText( + await app.expectAlertText( "Failed to build the hex file", "There is no storage space left." ); @@ -47,13 +47,13 @@ test.describe("save", () => { app, }) => { await app.save({ waitForDownload: false }); - await app.confirmInputDialog("Name your project"); + await app.expectDialog("Name your project"); }); test("Shows the post-save dialog after hex save", async ({ app }) => { await app.setProjectName("not default name"); await app.save(); - await app.confirmInputDialog("Project saved"); + await app.expectDialog("Project saved"); }); test("Shows the multiple files dialog after main.py save if there are multiple files in the project", async ({ @@ -63,7 +63,7 @@ test.describe("save", () => { await app.loadFiles("testData/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); - await app.saveMain(); - await app.confirmInputDialog("Warning: Only main.py downloaded"); + await app.savePythonScript(); + await app.expectDialog("Warning: Only main.py downloaded"); }); }); diff --git a/src/e2e/simulator.test.ts b/src/e2e/simulator.test.ts index 62839be62..a49b6ac1a 100644 --- a/src/e2e/simulator.test.ts +++ b/src/e2e/simulator.test.ts @@ -21,37 +21,37 @@ test.describe("simulator", () => { // Enum sensor change via select and button. await app.selectAllInEditor(); await app.typeInEditor(gestureTest); - await app.runSimulator(); - await app.simulatorSelectGesture("freefall"); - await app.simulatorSendGesture(); - await app.simulatorConfirmResponse(); + await app.simulator.run(); + await app.simulator.simulatorSelectGesture("freefall"); + await app.simulator.sendGestureButton.click(); + await app.simulator.expectResponse(); }); test("responds to a range sensor change", async ({ app }) => { // Range sensor change via slider. await app.selectAllInEditor(); await app.typeInEditor(sliderTest); - await app.runSimulator(); - await app.simulatorSetRangeSlider("Temperature", "min"); - await app.simulatorConfirmResponse(); + await app.simulator.run(); + await app.simulator.setRangeSlider("Temperature", "min"); + await app.simulator.expectResponse(); }); test("responds to a button press", async ({ app }) => { // Range sensor change via button. await app.selectAllInEditor(); await app.typeInEditor(buttonTest); - await app.runSimulator(); - await app.simulatorInputPressHold("Press button A", 500); - await app.simulatorConfirmResponse(); + await app.simulator.run(); + await app.simulator.inputPressHold("Press button A", 500); + await app.simulator.expectResponse(); }); test("stops when the code changes", async ({ app }) => { await app.selectAllInEditor(); await app.typeInEditor(basicTest); - await app.runSimulator(); - await app.simulatorConfirmResponse(); + await app.simulator.run(); + await app.simulator.expectResponse(); await app.typeInEditor("A change!"); - await app.findStoppedSimulator(); + await app.simulator.expectStopped(); }); }); From 1cb5d93f731e9999bdb20457cae34b7a5a4b3abb Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 10:50:36 +0000 Subject: [PATCH 23/38] Make playwright versions the same in package.json --- package-lock.json | 41 +++++++---------------------------------- package.json | 2 +- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7508e30a7..65c078e6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", - "playwright": "^1.42.1", + "playwright": "1.33.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", @@ -8889,27 +8889,24 @@ } }, "node_modules/playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.33.0.tgz", + "integrity": "sha512-+zzU3V2TslRX2ETBRgQKsKytYBkJeLZ2xzUj4JohnZnxQnivoUvOvNbRBYWSYykQTO0Y4zb8NwZTYFUO+EpPBQ==", + "hasInstallScript": true, "dependencies": { - "playwright-core": "1.42.1" + "playwright-core": "1.33.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "node": ">=14" } }, "node_modules/playwright-core": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.33.0.tgz", "integrity": "sha512-aizyPE1Cj62vAECdph1iaMILpT0WUDCq3E6rW6I+dleSbBoGbktvJtzS6VHkZ4DKNEOG9qJpiom/ZxO+S15LAw==", - "dev": true, "bin": { "playwright": "cli.js" }, @@ -8917,30 +8914,6 @@ "node": ">=14" } }, - "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==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright/node_modules/playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/package.json b/package.json index 089de5836..94adfaed7 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", - "playwright": "^1.42.1", + "playwright": "1.33.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", From d5609928a7b9e4d200d765fe4b927ee55130b7f7 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 10:52:57 +0000 Subject: [PATCH 24/38] Update build.yml --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 413b03a36..0511e8eb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,14 +51,17 @@ jobs: if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' - run: curl --insecure -4 --retry 7 --retry-connrefused http://localhost:3000 1>/dev/null if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' - - run: npm run test:e2e:headless + - name: Run Playwright tests + run: npm run test:e2e:headless + uses: docker://mcr.microsoft.com/playwright:v1.39.0-jammy if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' - name: Store reports if: (env.STAGE == 'REVIEW' || env.STAGE == 'STAGING') && failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: reports path: reports/ + retention-days: 3 - run: npm run deploy if: github.repository_owner == 'microbit-foundation' && (env.STAGE == 'REVIEW' || success()) env: From b86d830e5c1d4e4ca350b67d7a496408a054863f Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 10:55:23 +0000 Subject: [PATCH 25/38] Fix build.yml --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0511e8eb8..dd9212773 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,9 +52,10 @@ jobs: - run: curl --insecure -4 --retry 7 --retry-connrefused http://localhost:3000 1>/dev/null if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' - name: Run Playwright tests - run: npm run test:e2e:headless - uses: docker://mcr.microsoft.com/playwright:v1.39.0-jammy if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' + uses: docker://mcr.microsoft.com/playwright:v1.39.0-jammy + with: + args: npx playwright test - name: Store reports if: (env.STAGE == 'REVIEW' || env.STAGE == 'STAGING') && failure() uses: actions/upload-artifact@v4 From 731fe3e779405253f6f13aa554b8013ce607bbd2 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 11:03:07 +0000 Subject: [PATCH 26/38] Rerun CI --- src/e2e/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e2e/app.ts b/src/e2e/app.ts index a7f85ea39..ee115a53b 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -367,7 +367,7 @@ export class App { const filePath = getAbsoluteFilePath(filePathFromProjectRoot); const filename = getFilename(filePathFromProjectRoot); - // wait for page to load + // Wait for page to load await this.saveButton.waitFor(); // Playwright drag and drop file method taken from From 6a9860282f7c72d17adb1658a28556fa2de270f4 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 11:25:06 +0000 Subject: [PATCH 27/38] Upgrade to playwright@latest --- package-lock.json | 69 +++++++++++++++++++++++------------------------ package.json | 4 +-- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65c078e6a..de969052f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", - "playwright": "1.33.0", + "playwright": "^1.42.1", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", @@ -64,7 +64,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "1.33.0", + "@playwright/test": "^1.42.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", @@ -2837,36 +2837,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.33.0.tgz", - "integrity": "sha512-YunBa2mE7Hq4CfPkGzQRK916a4tuZoVx/EpLjeWlTVOnD4S2+fdaQZE0LJkbfhN5FTSKNLdcl7MoT5XB37bTkg==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", "dev": true, "dependencies": { - "@types/node": "*", - "playwright-core": "1.33.0" + "playwright": "1.42.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/@playwright/test/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": ">=16" } }, "node_modules/@popperjs/core": { @@ -8889,29 +8871,44 @@ } }, "node_modules/playwright": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.33.0.tgz", - "integrity": "sha512-+zzU3V2TslRX2ETBRgQKsKytYBkJeLZ2xzUj4JohnZnxQnivoUvOvNbRBYWSYykQTO0Y4zb8NwZTYFUO+EpPBQ==", - "hasInstallScript": true, + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", "dependencies": { - "playwright-core": "1.33.0" + "playwright-core": "1.42.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.33.0.tgz", - "integrity": "sha512-aizyPE1Cj62vAECdph1iaMILpT0WUDCq3E6rW6I+dleSbBoGbktvJtzS6VHkZ4DKNEOG9qJpiom/ZxO+S15LAw==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "bin": { - "playwright": "cli.js" + "playwright-core": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" + } + }, + "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==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/possible-typed-array-names": { diff --git a/package.json b/package.json index 94adfaed7..4b6c1ba02 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", - "playwright": "1.33.0", + "playwright": "^1.42.1", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", @@ -60,7 +60,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@chakra-ui/cli": "^2.4.1", "@formatjs/cli": "^6.2.7", - "@playwright/test": "1.33.0", + "@playwright/test": "^1.42.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", From 918d530e6288985db2eda5e6e69d2a5786995ceb Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 11:30:14 +0000 Subject: [PATCH 28/38] Upgrade to v1.42.1-jammy --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd9212773..78d91b65a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' - name: Run Playwright tests if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING' - uses: docker://mcr.microsoft.com/playwright:v1.39.0-jammy + uses: docker://mcr.microsoft.com/playwright:v1.42.1-jammy with: args: npx playwright test - name: Store reports From be59f1d88b7735e272528b00d43b2316dae8452b Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 12:52:52 +0000 Subject: [PATCH 29/38] Tweak tests to be less flaky --- src/e2e/app.ts | 39 ++++++++++++++++++++++------------ src/e2e/autocomplete.test.ts | 21 +++++++++++------- src/e2e/multiple-files.test.ts | 2 -- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/e2e/app.ts b/src/e2e/app.ts index ee115a53b..3ab33db90 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -84,7 +84,6 @@ class FileActionsMenu { } async delete() { - await this.deleteButton.waitFor(); await this.deleteButton.click(); await this.page.getByRole("button", { name: "Delete" }).click(); } @@ -102,9 +101,12 @@ class ProjectTabPanel { const fileActionsMenu = this.page.getByRole("button", { name: `${filename} file actions`, }); + const actionMenu = new FileActionsMenu(this.page, filename); await fileActionsMenu.waitFor(); + await fileActionsMenu.hover(); await fileActionsMenu.click(); - return new FileActionsMenu(this.page, filename); + await actionMenu.editButton.waitFor(); + return actionMenu; } async chooseFile(filePathFromProjectRoot: string) { @@ -312,10 +314,13 @@ export class App { } async typeInEditor(text: string): Promise { - const textWithoutLastChar = text.slice(0, text.length - 1); + const textWithoutLastChar = text.slice(0, text.length - 3); await this.editorTextArea.fill(textWithoutLastChar); - // Last character is typed separately to trigger editor suggestions - await this.page.keyboard.press(text.slice(-1)); + // Last few characters are typed separately to trigger editor suggestions + const [a, b, c] = text.slice(-3); + await this.page.keyboard.press(a, { delay: 500 }); + await this.page.keyboard.press(b, { delay: 500 }); + await this.page.keyboard.press(c, { delay: 500 }); } async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { @@ -480,11 +485,16 @@ export class App { async closeAndExpectBeforeUnloadDialogVisible( visible: boolean ): Promise { - this.page.on("dialog", async (dialog) => { - expect(dialog.type() === "beforeunload").toEqual(visible); - await dialog.dismiss(); - }); - this.page.close({ runBeforeUnload: true }); + if (visible) { + this.page.on("dialog", async (dialog) => { + expect(dialog.type() === "beforeunload").toEqual(visible); + + // Though https://playwright.dev/docs/api/class-page#page-event-dialog + // says that dialog.dismiss() is needed otherwise the page will freeze, + // in practice, it appears that the dialog is dismissed automatically. + }); + } + await this.page.close({ runBeforeUnload: true }); } async expectDocumentationTopLevelHeading( @@ -536,8 +546,7 @@ export class App { async dragDropCodeEmbed(name: string, targetLine: number) { const codeExample = this.getCodeExample(name); - const editorLine = this.page - .getByTestId("editor") + const editorLine = this.editor .getByRole("textbox") .locator("div") .filter({ hasText: targetLine.toString() }); @@ -628,6 +637,7 @@ export class App { async expectCompletionOptions(expected: string[]): Promise { const completions = this.page.getByRole("listbox", { name: "Completions" }); + await completions.waitFor(); const contents = await completions.innerText(); expect(contents).toEqual(expected.join("\n")); } @@ -637,13 +647,16 @@ export class App { .locator("div") .filter({ hasText: signature }) .nth(2); + await activeOption.waitFor(); await expect(activeOption).toBeVisible(); } async acceptCompletion(name: string): Promise { // This seems significantly more reliable than pressing Enter, though there's // no real-life issue here. - await this.editor.getByRole("option", { name }).click(); + const option = this.editor.getByRole("option", { name }); + await option.waitFor(); + await option.click(); } async followCompletionOrSignatureDocumentionLink( diff --git a/src/e2e/autocomplete.test.ts b/src/e2e/autocomplete.test.ts index 9ae8219bf..e6385a649 100644 --- a/src/e2e/autocomplete.test.ts +++ b/src/e2e/autocomplete.test.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: MIT */ import { test } from "./app-test-fixtures.js"; +import { expect } from "@playwright/test"; -const showFullSignature = - "show(image, delay=400, wait=True, loop=False, clear=False)"; +const showSignature = "show(image, delay=400, wait="; test.describe("autocomplete", () => { test("shows autocomplete as you type", async ({ app }) => { @@ -44,7 +44,7 @@ test.describe("autocomplete", () => { await app.followCompletionOrSignatureDocumentionLink("API"); - await app.expectActiveApiEntry(showFullSignature); + await app.expectActiveApiEntry(showSignature); }); test("autocomplete can navigate to Reference toolkit content", async ({ @@ -62,7 +62,7 @@ test.describe("autocomplete", () => { await app.typeInEditor("from microbit import *\ndisplay.sho"); await app.acceptCompletion("show"); - await app.expectSignatureHelp(showFullSignature); + await app.expectSignatureHelp(showSignature); }); test("does not insert brackets for import completion", async ({ app }) => { @@ -79,11 +79,16 @@ test.describe("autocomplete", () => { // The closing bracket is autoinserted. await app.typeInEditor("from microbit import *\ndisplay.show("); - await app.expectSignatureHelp(showFullSignature); - + const signatureHelp = app.page + .getByTestId("editor") + .locator("div") + .filter({ hasText: showSignature }) + .nth(1); + await signatureHelp.waitFor(); + await expect(signatureHelp).toBeVisible(); await app.followCompletionOrSignatureDocumentionLink("API"); - await app.expectActiveApiEntry(showFullSignature); + await app.expectActiveApiEntry(showSignature); }); test("signature can navigate to Reference toolkit content", async ({ @@ -92,7 +97,7 @@ test.describe("autocomplete", () => { await app.selectAllInEditor(); // The closing bracket is autoinserted. await app.typeInEditor("from microbit import *\ndisplay.show("); - await app.expectSignatureHelp(showFullSignature); + await app.expectSignatureHelp(showSignature); await app.followCompletionOrSignatureDocumentionLink("Help"); await app.expectActiveApiEntry("Show"); }); diff --git a/src/e2e/multiple-files.test.ts b/src/e2e/multiple-files.test.ts index d5dc69df0..b87e4f6a8 100644 --- a/src/e2e/multiple-files.test.ts +++ b/src/e2e/multiple-files.test.ts @@ -68,9 +68,7 @@ test.describe("multiple-files", () => { acceptDialog: LoadDialogType.CONFIRM, }); await app.editFile("module.py"); - await app.deleteFile("module.py"); - await app.expectEditorContainText(/Hello/); }); From 89c6abbcfbbe5acbe4dd90b1d3fe9cd4453472e3 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 12:55:53 +0000 Subject: [PATCH 30/38] Add timeout minutes to build time --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78d91b65a..b82cc1859 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,7 @@ concurrency: jobs: build: + timeout-minutes: 15 runs-on: ubuntu-latest permissions: contents: read From c580a85be6d86d8171812d5c98d33874f0b98d93 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 13:26:46 +0000 Subject: [PATCH 31/38] Remove puppeteer and pptr-testing-library --- package-lock.json | 682 ---------------------------------------------- package.json | 2 - 2 files changed, 684 deletions(-) diff --git a/package-lock.json b/package-lock.json index de969052f..a0480b7ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,9 +80,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "jsdom": "^24.0.0", - "pptr-testing-library": "^0.7.0", "prettier": "2.3.2", - "puppeteer": "13.7.0", "typescript": "^5.4.2", "vite-plugin-svgr": "^4.2.0", "vitest": "^1.3.1" @@ -526,19 +524,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.24.0.tgz", - "integrity": "sha512-HxiRMOncx3ly6f3fcZ1GVKf+/EROcI9qwPgmij8Czqy6Okm/0T37T4y2ZIlLUuEUFjtM7NRsfdCO8Y3tAiJZew==", - "dev": true, - "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", @@ -3831,16 +3816,6 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", @@ -4710,17 +4685,6 @@ "node": ">=8" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4780,39 +4744,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4956,12 +4887,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5229,17 +5154,6 @@ "toggle-selection": "^1.0.6" } }, - "node_modules/core-js-pure": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.36.0.tgz", - "integrity": "sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ==", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -5278,15 +5192,6 @@ "yarn": ">=1" } }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "dependencies": { - "node-fetch": "2.6.7" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5479,12 +5384,6 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, - "node_modules/devtools-protocol": { - "version": "0.0.981744", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.981744.tgz", - "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", - "dev": true - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -5560,15 +5459,6 @@ "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==", "dev": true }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -6267,26 +6157,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6330,15 +6200,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6403,19 +6264,6 @@ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -6521,12 +6369,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6626,21 +6468,6 @@ "node": ">=6" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -6925,26 +6752,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -8155,18 +7962,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -8371,12 +8166,6 @@ "node": "*" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, "node_modules/mlly": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", @@ -8433,48 +8222,6 @@ "tslib": "^2.0.3" } }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -8693,42 +8440,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8824,12 +8535,6 @@ "node": "*" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -8847,18 +8552,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", @@ -8947,176 +8640,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/pptr-testing-library": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/pptr-testing-library/-/pptr-testing-library-0.7.0.tgz", - "integrity": "sha512-NYt6XQzAoWCC/WKkBWW40Uth+MBRKmdYr+3NdrF4gTgBeK31zNQN6gFvmTubjZY5mUVdHmPns60jTs7PZuwg2A==", - "dev": true, - "dependencies": { - "@testing-library/dom": "^7.31.0", - "wait-for-expect": "^3.0.2" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "puppeteer": "*" - } - }, - "node_modules/pptr-testing-library/node_modules/@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/pptr-testing-library/node_modules/@testing-library/dom": { - "version": "7.31.2", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", - "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^4.2.2", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.6", - "lz-string": "^1.4.4", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pptr-testing-library/node_modules/@types/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", - "dev": true - }, - "node_modules/pptr-testing-library/node_modules/@types/yargs": { - "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/pptr-testing-library/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pptr-testing-library/node_modules/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/pptr-testing-library/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/pptr-testing-library/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/pptr-testing-library/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/pptr-testing-library/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pptr-testing-library/node_modules/pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "dependencies": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/pptr-testing-library/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "node_modules/pptr-testing-library/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9170,15 +8693,6 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9189,28 +8703,12 @@ "react-is": "^16.13.1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9220,77 +8718,6 @@ "node": ">=6" } }, - "node_modules/puppeteer": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-13.7.0.tgz", - "integrity": "sha512-U1uufzBjz3+PkpCxFrWzh4OrMIdIb2ztzCu0YEPfRHjHswcSwHZswnK+WdsOQJsRV8WeTg3jLhJR4D867+fjsA==", - "deprecated": "< 21.5.0 is no longer supported", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.981744", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.5.0" - }, - "engines": { - "node": ">=10.18.1" - } - }, - "node_modules/puppeteer/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/puppeteer/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/puppeteer/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -9493,20 +8920,6 @@ } } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9709,26 +9122,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -9950,15 +9343,6 @@ "node": ">= 0.4" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -10134,34 +9518,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/text-encoder-lite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/text-encoder-lite/-/text-encoder-lite-2.0.0.tgz", @@ -10173,12 +9529,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -10417,16 +9767,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -10531,12 +9871,6 @@ } } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, "node_modules/vite": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", @@ -11098,12 +10432,6 @@ "node": ">=18" } }, - "node_modules/wait-for-expect": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz", - "integrity": "sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==", - "dev": true - }, "node_modules/web-vitals": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-1.1.2.tgz", @@ -11331,16 +10659,6 @@ "node": ">= 6" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", diff --git a/package.json b/package.json index 4b6c1ba02..e45123c4f 100644 --- a/package.json +++ b/package.json @@ -76,9 +76,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "jsdom": "^24.0.0", - "pptr-testing-library": "^0.7.0", "prettier": "2.3.2", - "puppeteer": "13.7.0", "typescript": "^5.4.2", "vite-plugin-svgr": "^4.2.0", "vitest": "^1.3.1" From 179e918954ddcdffc85aaf2646eaf09c354e6dd3 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 13:27:39 +0000 Subject: [PATCH 32/38] Rename last `findXXX` to `expectXXX` --- src/e2e/app.ts | 2 +- src/e2e/multiple-files.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 3ab33db90..8f8bb9d85 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -428,7 +428,7 @@ export class App { await fileOptionMenu.editButton.click(); } - async findThirdPartyModuleWarning( + async expectThirdPartModuleWarning( expectedName: string, expectedVersion: string ): Promise { diff --git a/src/e2e/multiple-files.test.ts b/src/e2e/multiple-files.test.ts index b87e4f6a8..a33f94bfa 100644 --- a/src/e2e/multiple-files.test.ts +++ b/src/e2e/multiple-files.test.ts @@ -48,7 +48,7 @@ test.describe("multiple-files", () => { acceptDialog: LoadDialogType.CONFIRM, }); await app.editFile("module.py"); - await app.findThirdPartyModuleWarning("a", "1.0.0"); + await app.expectThirdPartModuleWarning("a", "1.0.0"); await app.toggleSettingThirdPartyModuleEditing(); try { @@ -60,7 +60,7 @@ test.describe("multiple-files", () => { await app.loadFiles("testData/updated/module.py", { acceptDialog: LoadDialogType.CONFIRM, }); - await app.findThirdPartyModuleWarning("a", "1.1.0"); + await app.expectThirdPartModuleWarning("a", "1.1.0"); }); test("Copes with currently open file being deleted", async ({ app }) => { From 8c5684f6cb524b038bba44d3bdb4527a700001de Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 13:35:20 +0000 Subject: [PATCH 33/38] Use playwright instead of vitest for E2E Removes E2E config for vitest/vite --- package.json | 4 ++-- vite.config.ts | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e45123c4f..d2d3b613f 100644 --- a/package.json +++ b/package.json @@ -98,8 +98,8 @@ "postinstall": "npm run theme", "serve": "npx serve --no-clipboard -l 3000 -- build/", "start": "vite dev", - "test:e2e:headless": "cross-env E2E_HEADLESS=1 vitest --mode e2e", - "test:e2e": "npx playwright test --ui", + "test:e2e:headless": "playwright test", + "test:e2e": "playwright test --ui", "test": "vitest", "theme:watch": "chakra-cli tokens src/deployment/default/theme.ts --watch", "theme": "chakra-cli tokens src/deployment/default/theme.ts", diff --git a/vite.config.ts b/vite.config.ts index 34aa4cc8d..6a5806f2b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -36,13 +36,6 @@ export default defineConfig(({ mode }) => { setupFiles: "./src/setupTests.ts", mockReset: true, }; - const e2eTest: UserConfig["test"] = { - globals: true, - include: ["src/e2e/**/*.test.ts"], - environment: "jsdom", - testTimeout: 60_000, - hookTimeout: 30_000, - }; const config: UserConfig = { base: process.env.BASE_URL ?? "/", build: { @@ -60,7 +53,7 @@ export default defineConfig(({ mode }) => { react(), svgr(), ], - test: mode === "e2e" ? e2eTest : unitTest, + test: unitTest, resolve: { alias: { "theme-package": fs.existsSync(external) From d6ea1ee96447bc70bec02bdc048b87510254ff41 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 13:48:36 +0000 Subject: [PATCH 34/38] Remove playwright from package.json --- package-lock.json | 4 +++- package.json | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0480b7ce..e8f6d3ba1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,6 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", - "playwright": "^1.42.1", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", @@ -8567,6 +8566,7 @@ "version": "1.42.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "dev": true, "dependencies": { "playwright-core": "1.42.1" }, @@ -8584,6 +8584,7 @@ "version": "1.42.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "dev": true, "bin": { "playwright-core": "cli.js" }, @@ -8595,6 +8596,7 @@ "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": [ diff --git a/package.json b/package.json index d2d3b613f..37475378a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "lzma": "^2.3.2", "marked": "^4.0.15", "mobile-drag-drop": "^2.3.0-rc.2", - "playwright": "^1.42.1", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.8.0", From 55117e19d395bd0a11920e4891275644f89a7df6 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 13:48:55 +0000 Subject: [PATCH 35/38] Refactor getSimulatorIframe --- src/e2e/app.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 8f8bb9d85..5c07b88c8 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -171,9 +171,7 @@ class Simulator { // Simulator functions private getSimulatorIframe(): Frame { - const simulatorIframe = this.page - .frames() - .find((frame) => frame.name() === "Simulator"); + const simulatorIframe = this.page.frame("Simulator"); if (!simulatorIframe) { throw new Error("Simulator iframe not found"); } From 2339e62c825de3547e28fda53b9894786f886bd0 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 13:59:51 +0000 Subject: [PATCH 36/38] Refactor typeInEditor --- src/e2e/app.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 5c07b88c8..721da1298 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -312,13 +312,14 @@ export class App { } async typeInEditor(text: string): Promise { - const textWithoutLastChar = text.slice(0, text.length - 3); - await this.editorTextArea.fill(textWithoutLastChar); + const numCharTyped = 2; + const textWithoutLastChars = text.slice(0, -numCharTyped); + const lastChars = text.slice(-numCharTyped); + await this.editorTextArea.fill(textWithoutLastChars); // Last few characters are typed separately to trigger editor suggestions - const [a, b, c] = text.slice(-3); - await this.page.keyboard.press(a, { delay: 500 }); - await this.page.keyboard.press(b, { delay: 500 }); - await this.page.keyboard.press(c, { delay: 500 }); + for (const char of lastChars) { + await this.page.keyboard.press(char, { delay: 500 }); + } } async switchTab(tabName: "Project" | "API" | "Reference" | "Ideas") { From 8962bb445104d7c64788fc6501260f624c90982b Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 14:08:01 +0000 Subject: [PATCH 37/38] Tweak typeInEditor comment --- src/e2e/app.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 721da1298..9e53d6b6b 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -316,7 +316,8 @@ export class App { const textWithoutLastChars = text.slice(0, -numCharTyped); const lastChars = text.slice(-numCharTyped); await this.editorTextArea.fill(textWithoutLastChars); - // Last few characters are typed separately to trigger editor suggestions + // Last few characters are typed separately and slower to + // reliably trigger editor suggestions for (const char of lastChars) { await this.page.keyboard.press(char, { delay: 500 }); } From 4a7c055ec90ffddf8080d49d677e321345585801 Mon Sep 17 00:00:00 2001 From: Grace Date: Tue, 19 Mar 2024 16:07:15 +0000 Subject: [PATCH 38/38] Add playwright to dev --- package-lock.json | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index e8f6d3ba1..b8c3cf4ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "jsdom": "^24.0.0", + "playwright": "^1.42.1", "prettier": "2.3.2", "typescript": "^5.4.2", "vite-plugin-svgr": "^4.2.0", diff --git a/package.json b/package.json index 37475378a..394c01504 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "jsdom": "^24.0.0", + "playwright": "^1.42.1", "prettier": "2.3.2", "typescript": "^5.4.2", "vite-plugin-svgr": "^4.2.0",