Unify Frontend App (#11287)

Fixes #10668
Fixes #8484

Summary of changes:
* `gui2` and `dashboard` are merged to `gui` directory. Various configs were merged (package.json, playwrigth, TS...). The src and e2e directories are split to `dashboard` and `project-view` for now.
* E2E tests run two servers on different ports. The tests are organized in projects. This is also to be changed soon, as we plan to [use better mocking in GUI/ProjectView](#9726)
* ESlint configs were merged to central `eslint.config.mjs`, and that file was moved to repository root. We kept the dashboard lints, but they can be relaxed. The dashboard code was changed to meet GUI lints.
* Also, the versions of linter plugins were bumped, and code fixed.
* The ide-desktop/client no longer has `dashboard` dependency - the only type used there was moved to common package.
* `common` package moved to `app`.
This commit is contained in:
Adam Obuchowicz 2024-10-11 20:23:02 +02:00 committed by GitHub
parent 204b37c6c3
commit 4a249688e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1130 changed files with 2794 additions and 4492 deletions

31
.github/CODEOWNERS vendored
View File

@ -19,14 +19,30 @@ Cargo.toml
/lib/rust/parser/ @farmaazon @kazcw @vitvakatu @Frizi @jaroslavtulach @AdRiley
/tools/build-performance/ @kazcw @Akirathan
# Global JS configuration
esling.config.mjs
tsconfig.json
# Scala Libraries
/lib/scala/ @4e6 @jaroslavtulach @hubertp @Akirathan
# Java libraries
/lib/java/ @4e6 @jaroslavtulach @hubertp @Akirathan
# GUI
/app/gui2/ @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
# GUI/Dashboard
/app @Frizi @farmaazon @vitvakatu @kazcw @AdRiley @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/gui/e2e/dashboard @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/gui/e2e/project-view @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
/app/gui/src/dashboard @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/gui/src/project-view @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
/app/ide-desktop/ @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/ydoc-server/ @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
/app/ydoc-server-nodejs/ @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
/app/ydoc-server-polyglot/ @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
/app/ydoc-shared/ @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
# The data-link schema is owned by the libraries team
/app/gui/src/dashboard/data/datalinkSchema.json @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey
/app/gui/src/dashboard/data/__tests__ @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
# Engine (old)
# This section should be removed once the engine moves to /app/engine
@ -45,14 +61,3 @@ Cargo.toml
# The default project template is owned by the libraries team
/lib/scala/pkg/src/main/resources/default/src/ @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey
# Dashboard, Cloud, Authentication & Electron
/app/ide-desktop/ @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/dashboard/ @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
# The data-link schema is owned by the libraries team
/app/dashboard/src/data/datalinkSchema.json @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey
/app/dashboard/src/data/__tests__ @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
# GUI / Dashboard shared
/app/*.* @Frizi @farmaazon @vitvakatu @kazcw @AdRiley @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount
/app/ide-desktop/common @PabloBuchu @indiv0 @somebody1234 @MrFlashAccount @Frizi @farmaazon @vitvakatu @kazcw @AdRiley

View File

@ -39,10 +39,10 @@ app/ide-desktop/lib/dashboard/playwright/.cache/
app/ide-desktop/lib/dashboard/dist/
app/gui/view/documentation/assets/stylesheet.css
app/rust-ffi/pkg
app/gui2/src/assets/font-*.css
app/gui/src/project-view/assets/font-*.css
Cargo.lock
build.json
app/gui2/playwright-report/
app/gui/playwright-report/
# Engine Builds can leave these nested working copies.
# TODO [mwu]: Adjust Engine build to not leave them.

View File

@ -12,7 +12,7 @@
}
],
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.experimental.useFlatConfig": true,
"eslint.useFlatConfig": true,
"eslint.useESLintClass": true,
"[javascript][typescript][typescriptreact][vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",

View File

@ -21,7 +21,7 @@
<img src="https://img.shields.io/static/v1?label=Compiler%20License&message=Apache%20v2&color=2ec352&labelColor=2c3239"
alt="License">
</a>
<a href="https://github.com/enso-org/enso/blob/develop/app/gui2/LICENSE">
<a href="https://github.com/enso-org/enso/blob/develop/app/gui/LICENSE">
<img src="https://img.shields.io/static/v1?label=GUI%20License&message=AGPL%20v3&color=2ec352&labelColor=2c3239"
alt="License">
</a>
@ -207,11 +207,11 @@ Enso consists of several sub projects:
command line tools.
- **Enso IDE:** The
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a
desktop application that allows working with the visual form of Enso. It
consists of an Electron application, a high performance WebGL UI framework,
and the searcher which provides contextual search, hints, and documentation
for all of Enso's functionality.
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui) is a desktop
application that allows working with the visual form of Enso. It consists of
an Electron application, a high performance WebGL UI framework, and the
searcher which provides contextual search, hints, and documentation for all of
Enso's functionality.
<br/>
@ -222,7 +222,7 @@ The Enso Engine is licensed under the
[LICENSE](https://github.com/enso-org/enso/blob/develop/LICENSE) file. The Enso
IDE is licensed under the [AGPL 3.0](https://opensource.org/licenses/AGPL-3.0),
as specified in the
[LICENSE](https://github.com/enso-org/enso/blob/develop/app/gui2/LICENSE) file.
[LICENSE](https://github.com/enso-org/enso/blob/develop/app/gui/LICENSE) file.
This license set was chosen to provide you with complete freedom to use Enso,
create libraries, and release them under any license of your choice, while also

View File

@ -46,7 +46,7 @@
"request": "launch",
"name": "GUI (Storybook)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "--filter", "enso-gui2", "story:dev"],
"runtimeArgs": ["run", "--filter", "enso-gui", "story:dev"],
"outputCapture": "std"
},
{
@ -70,7 +70,7 @@
"request": "launch",
"name": "GUI (E2E UI)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:e2e", "--", "--ui"],
"runtimeArgs": ["run", "--filter", "enso-gui", "test:e2e", "--", "--ui"],
"outputCapture": "std"
},
{
@ -102,14 +102,14 @@
"request": "launch",
"name": "GUI (All tests)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "--filter", "enso-gui2", "test"]
"runtimeArgs": ["run", "--filter", "enso-gui", "test"]
},
{
"type": "node",
"request": "launch",
"name": "GUI (E2E tests)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:e2e"],
"runtimeArgs": ["run", "--filter", "enso-gui", "test:e2e"],
"outputCapture": "std"
},
{
@ -117,7 +117,7 @@
"request": "launch",
"name": "GUI (Unit tests)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:unit", "--", "run"],
"runtimeArgs": ["run", "--filter", "enso-gui", "test:unit", "--", "run"],
"outputCapture": "std"
}
]

View File

@ -6,6 +6,7 @@
"exports": {
".": "./src/index.js",
"./src/config.json": "./src/config.json",
"./src/accessToken": "./src/accessToken.ts",
"./src/appConfig": "./src/appConfig.js",
"./src/buildUtils": "./src/buildUtils.js",
"./src/detect": "./src/detect.ts",

View File

@ -445,8 +445,7 @@ export interface CheckoutSessionStatus {
/** Status of the payment for the checkout session. */
readonly paymentStatus: string
/** Status of the checkout session. */
// eslint-disable-next-line @typescript-eslint/ban-types
readonly status: 'active' | 'trialing' | (string & {})
readonly status: 'active' | 'trialing' | (string & NonNullable<unknown>)
}
/** Resource usage of a VM. */

View File

@ -1,4 +0,0 @@
playwright-report/
playwright/.cache/
test-results/
dist/

View File

@ -1,44 +0,0 @@
<!--
FIXME [NP]: https://github.com/enso-org/cloud-v2/issues/345
This file is used by both the `content` and `dashboard` packages. The `dashboard` package uses it
via a symlink. This is temporary, while the `content` and `dashboard` have separate entrypoints
for cloud and desktop. Once they are merged, the symlink must be removed.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- FIXME https://github.com/validator/validator/issues/917 -->
<!-- FIXME Security Vulnerabilities: https://github.com/enso-org/ide/issues/226 -->
<!-- NOTE `frame-src` section of `http-equiv` required only for authorization -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
frame-src 'self' data: https://accounts.google.com https://enso-org.firebaseapp.com;
script-src 'self' 'unsafe-eval' data: https://*;
style-src 'self' 'unsafe-inline' data: https://*;
connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*;
worker-src 'self' blob:;
img-src 'self' blob: data: https://*;
font-src 'self' data: https://*"
/>
<meta
name="viewport"
content="
width=device-width,
initial-scale = 1.0,
maximum-scale = 1.0,
user-scalable = no"
/>
<title>Enso</title>
<script type="module" src="./src/entrypoint.ts" defer></script>
</head>
<body>
<div id="enso-dashboard" class="enso-dashboard"></div>
<div id="enso-chat" class="enso-chat"></div>
<div id="enso-portal-root" class="enso-portal-root"></div>
<noscript> This page requires JavaScript to run. Please enable it in your browser. </noscript>
</body>
</html>

View File

@ -1,76 +0,0 @@
# Dashboard
The dashboard is the entrypoint into the application. It includes project
management, project sharing, and user accounts and authentication.
## Further documentation
Further documentation is provided in the `docs/` folder:
- [Browser-specific behavior](./docs/browser_specific_behavior.md) details
behavior that is inconsistent between browsers and needs to be worked around.
## Folder structure
- `mock/`: Overrides for specific files in `src/` when running Playwright tests.
- `e2e/`: Contains end-to-end tests.
- `**/__tests__/`: Contains all unit tests. Unit tests MUST be in a `__tests__/`
subfolder, not beside (and not inside) the module they are testing.
- `src/`: The dashboard application.
- `index.html`: The sole HTML file used by this SPA. It imports the TS entry
point.
- `authentication/src/`: The main body of the app.
- `index.tsx`: The TS entry point.
- `providers/`: Contains React `Context`s used by the main app.
- `components/`: Contains the root component for the app.
- `dashboard/`: The main body of the app. Directly in the folder, there are
some utility modules that do not belong elsewhere.
- `components/`: Contains all components used by the main app.
- `events/`: Custom discriminated unions used to communicate messages
between unrelated components.
- `authentication/`: The authentication flow. This includes login,
registration, and changing passwords.
- `components/`: Contains all components used by the authentication flow.
- `providers/`: Contains React `Context`s required for authentication, and
used by the main app.
- `index.html`: The entrypoint, in the format required by Vite.
- `404.html`: A copy of the entrypoint. This is served on unknown routes by
certain static hosting providers.
- `esbuild-config.ts`: Configuration for ESBuild based on the environment
variables. This is a dependency of `esbuild-config.ts` in sibling modules.
## Cloud environment variables
These are environment variables related to the cloud backend. If these variables
are not set, the build will still work, however access to the cloud backend will
be disabled.
Note that `ENSO_CLOUD_ENVIRONMENT` may be set to instead load the files from a
`.env` file. If `ENSO_CLOUD_ENVIRONMENT` is not set, or it is `production` or
`''`, then variables are attempted to be read from `.env`. If it is set to any
other value (say, `foo`), then it is loaded from `.foo.env`.
(While the convention in the Node.js ecosystem is to name the variants like
`.env.foo`, `.foo.env` has been chosen here because `.env` should be more like
a file extension. Visual Studio Code also understands `.foo.env` but not
`.env.foo`.)
- `ENSO_CLOUD_REDIRECT`: The domain (or `localhost:8080`) where the login link
should redirect. Should include neither a path, nor a trailing slash.
- `ENSO_CLOUD_ENVIRONMENT`: The name of backend environment matching the
provided configuration keys. For most builds this should be `production`,
meaning that requests go to the production cloud backend.
- `ENSO_CLOUD_API_URL`: The root path for all API endpoints. Should not include
a trailing slash.
- `ENSO_CLOUD_SENTRY_DSN`: The Sentry Data Source Name (DSN) for this
environment. This should normally be the same for all environments.
- `ENSO_CLOUD_STRIPE_KEY`: Stripe's publishable client-side key.
- `ENSO_CLOUD_CHAT_URL`: The URL for the WebSocket server serving as the chat
backend.
- `ENSO_CLOUD_COGNITO_USER_POOL_ID`: The ID of the Cognito user pool.
- `ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID`: The client-side key of the
Cognito user pool.
- `ENSO_CLOUD_COGNITO_DOMAIN`: The domain which all Cognito requests should go
to.
- `ENSO_CLOUD_COGNITO_REGION`: The AWS region for which Cognito is configured.
Should match the region of the domain in `ENSO_CLOUD_COGNITO_DOMAIN`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,105 +0,0 @@
{
"name": "enso-dashboard",
"version": "0.1.0",
"type": "module",
"main": "./src/index.tsx",
"private": true,
"imports": {
"#/*": "./src/*"
},
"exports": {
".": "./src/index.tsx",
"./tailwind.config": "./tailwind.config.js",
"./src/platform": "./src/platform.ts",
"./src/tailwind.css": "./src/tailwind.css"
},
"scripts": {
"compile": "tsc",
"typecheck": "tsc",
"build": "vite build",
"lint": "eslint .",
"dev": "vite",
"dev:e2e": "vite -c vite.test.config.ts",
"dev:e2e:ci": "vite -c vite.test.config.ts build && vite preview --port 8080 --strictPort",
"test": "corepack pnpm run /^^^^test:.*/",
"test:unit": "vitest run",
"test-dev:unit": "vitest",
"test:e2e": "cross-env NODE_ENV=production playwright test",
"test-dev:e2e": "cross-env NODE_ENV=production playwright test --ui"
},
"dependencies": {
"@aws-amplify/auth": "5.6.5",
"@aws-amplify/core": "5.8.5",
"@hookform/resolvers": "^3.4.0",
"@internationalized/date": "^3.5.5",
"@monaco-editor/react": "4.6.0",
"@react-aria/interactions": "^3.22.3",
"@sentry/react": "^7.74.0",
"@stripe/react-stripe-js": "^2.7.1",
"@stripe/stripe-js": "^3.5.0",
"@tanstack/react-query": "5.55.0",
"@tanstack/vue-query": ">= 5.54.0 < 5.56.0",
"ajv": "^8.12.0",
"amazon-cognito-identity-js": "6.3.6",
"clsx": "^2.1.1",
"enso-common": "workspace:*",
"framer-motion": "11.3.0",
"input-otp": "1.2.4",
"is-network-error": "^1.0.1",
"monaco-editor": "0.48.0",
"qrcode.react": "3.1.0",
"react": "^18.3.1",
"react-aria": "^3.34.3",
"react-aria-components": "^1.3.3",
"react-dom": "^18.3.1",
"react-error-boundary": "4.0.13",
"react-hook-form": "^7.51.4",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1",
"react-stately": "^3.32.2",
"react-toastify": "^9.1.3",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "0.2.1",
"tiny-invariant": "^1.3.3",
"ts-results": "^3.3.0",
"validator": "^13.12.0",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
"@fast-check/vitest": "^0.0.8",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@playwright/test": "^1.40.0",
"@react-types/shared": "^3.22.1",
"@tanstack/react-query-devtools": "5.45.1",
"@types/eslint__js": "^8.42.3",
"@types/node": "^20.11.21",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/validator": "^13.11.7",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react": "^4.2.1",
"chalk": "^5.3.0",
"cross-env": "^7.0.3",
"enso-chat": "git://github.com/enso-org/enso-bot",
"eslint": "^8.49.0",
"eslint-plugin-react": "^7.32.1",
"fast-check": "^3.15.0",
"playwright": "^1.38.0",
"postcss": "^8.4.29",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-tailwindcss": "^0.5.11",
"react-toastify": "^9.1.3",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "1.0.7",
"tailwindcss-react-aria-components": "^1.1.1",
"typescript": "^5.5.3",
"vite": "^5.3.5",
"vitest": "^1.3.1"
},
"overrides": {
"@aws-amplify/auth": "../_IGNORED_",
"react-native-url-polyfill": "../_IGNORED_"
}
}

View File

@ -1,64 +0,0 @@
/** @file Playwright browser testing configuration. */
/** Note that running Playwright in CI poses a number of issues:
* - `backdrop-filter: blur` is disabled, due to issues with Chromium's `--disable-gpu` flag
* (see below).
* - System validation dialogs are not reliable between computers, as they may have different
* default fonts. */
import * as test from '@playwright/test'
import * as appConfig from 'enso-common/src/appConfig'
appConfig.loadTestEnvironmentVariables()
/* eslint-disable @typescript-eslint/no-magic-numbers, @typescript-eslint/strict-boolean-expressions */
const DEBUG = process.env.PWDEBUG === '1'
const TIMEOUT_MS = DEBUG ? 100_000_000 : 30_000
export default test.defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: true,
workers: process.env.PROD ? 8 : 1,
repeatEach: process.env.CI ? 3 : 1,
expect: {
toHaveScreenshot: { threshold: 0 },
timeout: TIMEOUT_MS,
},
timeout: TIMEOUT_MS,
reporter: 'html',
use: {
baseURL: 'http://localhost:8080',
trace: 'retain-on-failure',
launchOptions: {
ignoreDefaultArgs: ['--headless'],
args: [
...(DEBUG ?
[]
: [
// Much closer to headful Chromium than classic headless.
'--headless=new',
]),
// Required for `backdrop-filter: blur` to work.
'--use-angle=swiftshader',
// FIXME: `--disable-gpu` disables `backdrop-filter: blur`, which is not handled by
// the software (CPU) compositor. This SHOULD be fixed eventually, but this flag
// MUST stay as CI does not have a GPU.
'--disable-gpu',
// Fully disable GPU process.
'--disable-software-rasterizer',
// Disable text subpixel antialiasing.
'--font-render-hinting=none',
'--disable-skia-runtime-opts',
'--disable-system-font-check',
'--disable-font-subpixel-positioning',
'--disable-lcd-text',
],
},
},
webServer: {
command: `corepack pnpm run ${process.env.CI || process.env.PROD ? 'dev:e2e:ci' : 'dev:e2e'}`,
port: 8080,
reuseExistingServer: false,
},
})

View File

@ -1,9 +0,0 @@
/** @file Configuration for PostCSS. */
/* eslint-disable no-restricted-syntax */
export default {
plugins: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'tailwindcss/nesting': {},
tailwindcss: {},
},
}

View File

@ -1,8 +0,0 @@
/** @file Placeholder component for GUI used during e2e tests. */
import type * as editor from '#/layouts/Editor'
/** Placeholder component for GUI used during e2e tests. */
export function TestAppRunner(props: editor.GraphEditorProps) {
// eslint-disable-next-line no-restricted-syntax
return props.hidden ? <></> : <div data-testid="gui-editor-root">Vue app loads here.</div>
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,33 +0,0 @@
/** @file Entry point into the cloud dashboard. */
import * as commonQuery from 'enso-common/src/queryClient'
import '#/tailwind.css'
import * as main from '#/index'
import * as testAppRunner from '#/TestAppRunner'
// ===================
// === Entry point ===
// ===================
main.run({
logger: console,
// Browsers usually do not support vibrancy for webpages.
vibrancy: false,
// This file is only included when building for the cloud.
supportsLocalBackend: false,
supportsDeepLinks: false,
isAuthenticationDisabled: false,
shouldShowDashboard: true,
initialProjectName: null,
/** The `onAuthenticated` option is mandatory but is not needed here,
* so this function is empty. */
onAuthenticated() {
// eslint-disable-next-line @typescript-eslint/no-empty-function
},
/** The cloud frontend is not capable of running a Project Manager. */
projectManagerUrl: null,
ydocUrl: null,
appRunner: testAppRunner.TestAppRunner,
queryClient: commonQuery.createQueryClient(),
})

View File

@ -1,23 +0,0 @@
{
"extends": "../tsconfig.json",
"include": [
"src",
"e2e",
"../types",
"./src/**/*.json",
"./e2e/**/*.json",
"../../utils.ts",
".prettierrc.cjs",
"*.js",
"*.ts"
],
"exclude": ["./dist"],
"compilerOptions": {
"composite": true,
"noEmit": false,
"outDir": "../../node_modules/.cache/tsc",
"paths": { "#/*": ["./src/*"] },
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable", "ES2023"]
}
}

View File

@ -1,71 +0,0 @@
/** @file Configuration for vite. */
import * as fsSync from 'node:fs'
import * as url from 'node:url'
import vitePluginYaml from '@modyfi/vite-plugin-yaml'
import vitePluginReact from '@vitejs/plugin-react'
import * as vite from 'vite'
import * as common from 'enso-common'
import * as appConfig from 'enso-common/src/appConfig'
// =====================
// === Configuration ===
// =====================
const HTTP_STATUS_OK = 200
const SERVER_PORT = 8080
await appConfig.readEnvironmentFromFile()
/* eslint-disable @typescript-eslint/naming-convention */
export default vite.defineConfig({
server: { port: SERVER_PORT, headers: Object.fromEntries(common.COOP_COEP_CORP_HEADERS) },
plugins: [
vitePluginReact({
include: '**/*.tsx',
babel: { plugins: ['@babel/plugin-syntax-import-attributes'] },
}),
vitePluginYaml(),
serveFavicon(),
],
resolve: {
alias: {
'#': url.fileURLToPath(new URL('./src', import.meta.url)),
},
},
build: {
rollupOptions: {
input: {
main: url.fileURLToPath(new URL('./index.html', import.meta.url)),
'404': url.fileURLToPath(new URL('./404.html', import.meta.url)),
},
},
},
define: {
// The sole hardcoded usage of `global` in aws-amplify.
'global.TYPED_ARRAY_SUPPORT': JSON.stringify(true),
...appConfig.getDefines(),
},
})
/** A plugin to serve a favicon, in development mode only. */
function serveFavicon(): vite.Plugin {
const favicon = fsSync.readFileSync(url.fileURLToPath(new URL('./favicon.ico', import.meta.url)))
const headers: HeadersInit = [
['Content-Length', String(favicon.length)],
['Content-Type', 'image/png'],
...common.COOP_COEP_CORP_HEADERS,
]
return {
name: 'serve-favicon',
configureServer: (server) => {
server.middlewares.use((req, res, next) => {
if (req.url === '/favicon.ico') {
res.writeHead(HTTP_STATUS_OK, headers).end(favicon)
} else {
next()
}
})
},
}
}

View File

@ -1,25 +0,0 @@
/** @file Configuration for vitest. */
import * as url from 'node:url'
import * as vitestConfig from 'vitest/config'
import * as appConfig from 'enso-common/src/appConfig'
appConfig.loadTestEnvironmentVariables()
// @ts-expect-error This is required, otherwise importing node modules is broken.
// This is required for `datalinkSchema.test.ts`.
process.env.NODE_ENV = 'development'
const VITE_CONFIG = (await import('./vite.config')).default
export default vitestConfig.mergeConfig(
VITE_CONFIG,
vitestConfig.defineConfig({
test: {
environment: 'jsdom',
exclude: [...vitestConfig.configDefaults.exclude, '**/*.spec.{ts,tsx}'],
root: url.fileURLToPath(new URL('./', import.meta.url)),
restoreMocks: true,
},
}),
)

View File

@ -8,6 +8,7 @@ node_modules
dist
dist-ssr
coverage
mockDist
*.local
*.tsbuildinfo
@ -25,12 +26,11 @@ coverage
test-results/
playwright-report/
src/util/iconList.json
src/util/iconName.ts
src/stores/visualization/metadata.json
public/font-dejavu/
public/font-enso/
public/font-mplus1/
src/assets/font-dejavu.css
src/assets/font-enso.css
src/assets/font-mplus1.css
src/project-view/assets/font-dejavu.css
src/project-view/assets/font-enso.css
src/project-view/assets/font-mplus1.css
src/project-view/util/iconList.json
src/project-view/util/iconName.ts

View File

@ -3,7 +3,7 @@ import * as test from '@playwright/test'
import type * as inputBindings from '#/utilities/inputBindings'
import { modModifier } from '../actions'
import { modModifier } from '.'
// ====================
// === PageCallback ===
@ -27,7 +27,8 @@ export interface LocatorCallback {
// === BaseActions ===
// ===================
/** The base class from which all `Actions` classes are derived.
/**
* The base class from which all `Actions` classes are derived.
* It contains method common to all `Actions` subclasses.
* This is a [`thenable`], so it can be used as if it was a {@link Promise}.
*
@ -40,14 +41,18 @@ export default class BaseActions implements Promise<void> {
private readonly promise = Promise.resolve(),
) {}
/** Get the string name of the class of this instance. Required for this class to implement
* {@link Promise}. */
/**
* Get the string name of the class of this instance. Required for this class to implement
* {@link Promise}.
*/
get [Symbol.toStringTag]() {
return this.constructor.name
}
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
/**
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
static press(page: test.Page, keyOrShortcut: string): Promise<void> {
return test.test.step(`Press '${keyOrShortcut}'`, async () => {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
@ -77,18 +82,22 @@ export default class BaseActions implements Promise<void> {
return await this.promise.then(onfulfilled, onrejected)
}
/** Proxies the `catch` method of the internal {@link Promise}.
/**
* Proxies the `catch` method of the internal {@link Promise}.
* This method is not required for this to be a `thenable`, but it is still useful
* to treat this class as a {@link Promise}. */
* to treat this class as a {@link Promise}.
*/
// The following types are copied almost verbatim from the type definitions for `Promise`.
// eslint-disable-next-line no-restricted-syntax
async catch<T>(onrejected?: ((reason: unknown) => PromiseLike<T> | T) | null | undefined) {
return await this.promise.catch(onrejected)
}
/** Proxies the `catch` method of the internal {@link Promise}.
/**
* Proxies the `catch` method of the internal {@link Promise}.
* This method is not required for this to be a `thenable`, but it is still useful
* to treat this class as a {@link Promise}. */
* to treat this class as a {@link Promise}.
*/
async finally(onfinally?: (() => void) | null | undefined): Promise<void> {
await this.promise.finally(onfinally)
}
@ -101,9 +110,11 @@ export default class BaseActions implements Promise<void> {
return new clazz(this.page, this.promise, ...args)
}
/** Perform an action on the current page. This should generally be avoided in favor of using
/**
* Perform an action on the current page. This should generally be avoided in favor of using
* specific methods; this is more or less an escape hatch used ONLY when the methods do not
* support desired functionality. */
* support desired functionality.
*/
do(callback: PageCallback): this {
// @ts-expect-error This is SAFE, but only when the constructor of this class has the exact
// same parameters as `BaseActions`.
@ -119,8 +130,10 @@ export default class BaseActions implements Promise<void> {
return this.do(() => test.test.step(name, () => callback(this.page)))
}
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
/**
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
press<Key extends string>(keyOrShortcut: inputBindings.AutocompleteKeybind<Key>) {
return this.do((page) => BaseActions.press(page, keyOrShortcut))
}
@ -157,8 +170,10 @@ export default class BaseActions implements Promise<void> {
})
}
/** Expect an input to have an error (or no error if the expected value is `null`).
* If the expected value is `undefined`, the assertion is skipped. */
/**
* Expect an input to have an error (or no error if the expected value is `null`).
* If the expected value is `undefined`, the assertion is skipped.
*/
expectInputError(testId: string, description: string, expected: string | null | undefined) {
if (expected === undefined) {
return this

View File

@ -12,7 +12,7 @@ import {
locateSecretNameInput,
locateSecretValueInput,
TEXT,
} from '../actions'
} from '.'
import type * as baseActions from './BaseActions'
import * as contextMenuActions from './contextMenuActions'
import EditorPageActions from './EditorPageActions'
@ -120,8 +120,10 @@ export default class DrivePageActions extends PageActions {
locateAssetRows(page).nth(index).click({ position: ASSET_ROW_SAFE_POSITION }),
)
},
/** Right click a specific row to bring up its context menu, or the context menu for multiple
* assets when right clicking on a selected asset when multiple assets are selected. */
/**
* Right click a specific row to bring up its context menu, or the context menu for multiple
* assets when right clicking on a selected asset when multiple assets are selected.
*/
rightClickRow(index: number) {
return self.step(`Right click drive table row #${index}`, (page) =>
locateAssetRows(page)
@ -164,8 +166,10 @@ export default class DrivePageActions extends PageActions {
}),
)
},
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets to show. */
/**
* A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets to show.
*/
expectPlaceholderRow() {
return self.step('Expect placeholder row', async (page) => {
await test.expect(locateAssetRows(page)).toHaveCount(0)
@ -174,8 +178,10 @@ export default class DrivePageActions extends PageActions {
await test.expect(nonAssetRows).toHaveText(/This folder is empty/)
})
},
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets in Trash. */
/**
* A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets in Trash.
*/
expectTrashPlaceholderRow() {
return self.step('Expect trash placeholder row', async (page) => {
await test.expect(locateAssetRows(page)).toHaveCount(0)

View File

@ -1,7 +1,7 @@
/** @file Available actions for the login page. */
import * as test from '@playwright/test'
import { TEXT, VALID_EMAIL } from '../actions'
import { TEXT, VALID_EMAIL } from '.'
import BaseActions, { type LocatorCallback } from './BaseActions'
import LoginPageActions from './LoginPageActions'

View File

@ -1,7 +1,7 @@
/** @file Available actions for the login page. */
import * as test from '@playwright/test'
import { TEXT, VALID_EMAIL, VALID_PASSWORD, passAgreementsDialog } from '../actions'
import { TEXT, VALID_EMAIL, VALID_PASSWORD, passAgreementsDialog } from '.'
import BaseActions, { type LocatorCallback } from './BaseActions'
import DrivePageActions from './DrivePageActions'
import ForgotPasswordPageActions from './ForgotPasswordPageActions'

View File

@ -1,7 +1,7 @@
/** @file Actions for a "new Data Link" modal. */
import type * as test from 'playwright/test'
import { TEXT } from '../actions'
import { TEXT } from '.'
import type * as baseActions from './BaseActions'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'

View File

@ -1,7 +1,7 @@
/** @file Available actions for the login page. */
import * as test from '@playwright/test'
import { TEXT, VALID_EMAIL, VALID_PASSWORD } from '../actions'
import { TEXT, VALID_EMAIL, VALID_PASSWORD } from '.'
import BaseActions, { type LocatorCallback } from './BaseActions'
import LoginPageActions from './LoginPageActions'

View File

@ -1,5 +1,5 @@
/** @file Actions for the fourth step of the "setup" page. */
import { TEXT } from '../actions'
import { TEXT } from '.'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'

View File

@ -1,5 +1,5 @@
/** @file Actions for the third step of the "setup" page. */
import { TEXT } from '../actions'
import { TEXT } from '.'
import BaseActions from './BaseActions'
import SetupTeamPageActions from './SetupTeamPageActions'

View File

@ -1,5 +1,5 @@
/** @file Actions for the third step of the "setup" page. */
import { TEXT } from '../actions'
import { TEXT } from '.'
import BaseActions from './BaseActions'
import SetupInvitePageActions from './SetupInvitePageActions'

View File

@ -1,7 +1,7 @@
/** @file Actions for the second step of the "setup" page. */
import { PLAN_TO_UPGRADE_LABEL_ID } from '#/modules/payments/constants'
import { Plan } from 'enso-common/src/services/Backend'
import { TEXT } from '../actions'
import { TEXT } from '.'
import BaseActions from './BaseActions'
import SetupDonePageActions from './SetupDonePageActions'
import SetupOrganizationPageActions from './SetupOrganizationPageActions'

View File

@ -1,5 +1,5 @@
/** @file Actions for the "setup" page. */
import { TEXT } from '../actions'
import { TEXT } from '.'
import BaseActions from './BaseActions'
import SetupDonePageActions from './SetupDonePageActions'

View File

@ -1,5 +1,5 @@
/** @file Actions for the "setup" page. */
import { TEXT } from '../actions'
import { TEXT } from '.'
import BaseActions from './BaseActions'
import SetupPlanPageActions from './SetupPlanPageActions'

View File

@ -1,5 +1,5 @@
/** @file Actions for the "home" page. */
import * as actions from '../actions'
import * as actions from '.'
import BaseActions from './BaseActions'
import DrivePageActions from './DrivePageActions'
import EditorPageActions from './EditorPageActions'

View File

@ -1,5 +1,5 @@
/** @file Actions for the context menu. */
import { TEXT } from '../actions'
import { TEXT } from '.'
import type * as baseActions from './BaseActions'
import type BaseActions from './BaseActions'
import EditorPageActions from './EditorPageActions'

View File

@ -4,9 +4,9 @@ import * as test from '@playwright/test'
import { TEXTS } from 'enso-common/src/text'
import DrivePageActions from './actions/DrivePageActions'
import LoginPageActions from './actions/LoginPageActions'
import * as apiModule from './api'
import * as apiModule from '../api'
import DrivePageActions from './DrivePageActions'
import LoginPageActions from './LoginPageActions'
/* eslint-disable @typescript-eslint/no-namespace */
@ -292,7 +292,7 @@ export function locateSamples(page: test.Locator | test.Page) {
/** Find an editor container (if any) on the current page. */
export function locateEditor(page: test.Page) {
// Test ID of a placeholder editor component used during testing.
return page.getByTestId('gui-editor-root')
return page.locator('.App')
}
/** Find an assets table (if any) on the current page. */
@ -315,16 +315,20 @@ export function locateAssetName(locator: test.Locator) {
return locator.locator('> :nth-child(1)')
}
/** Find assets table rows that represent directories that can be expanded (if any)
* on the current page. */
/**
* Find assets table rows that represent directories that can be expanded (if any)
* on the current page.
*/
export function locateExpandableDirectories(page: test.Page) {
// The icon is hidden when not hovered so `getByLabel` will not work.
// eslint-disable-next-line no-restricted-properties
return locateAssetRows(page).filter({ has: page.locator('[aria-label=Expand]') })
}
/** Find assets table rows that represent directories that can be collapsed (if any)
* on the current page. */
/**
* Find assets table rows that represent directories that can be collapsed (if any)
* on the current page.
*/
export function locateCollapsibleDirectories(page: test.Page) {
// The icon is hidden when not hovered so `getByLabel` will not work.
// eslint-disable-next-line no-restricted-properties
@ -390,9 +394,11 @@ export function locateExtraColumns(page: test.Page) {
return page.getByTestId('extra-columns')
}
/** Find a root directory dropzone (if any) on the current page.
/**
* Find a root directory dropzone (if any) on the current page.
* This is the empty space below the assets table, if it doesn't take up the whole screen
* vertically. */
* vertically.
*/
export function locateRootDirectoryDropzone(page: test.Page) {
// This has no identifying features.
return page.getByTestId('root-directory-dropzone')
@ -591,9 +597,11 @@ export namespace settings {
// === Visual layout utilities ===
// ===============================
/** Get the left side of the bounding box of an asset row. The locator MUST be for an asset row.
/**
* Get the left side of the bounding box of an asset row. The locator MUST be for an asset row.
* DO NOT assume the left side of the outer container will change. This means that it is NOT SAFE
* to do anything with the returned values other than comparing them. */
* to do anything with the returned values other than comparing them.
*/
export function getAssetRowLeftPx(locator: test.Locator) {
return locator.evaluate((el) => el.children[0]?.children[0]?.getBoundingClientRect().left ?? 0)
}
@ -688,8 +696,10 @@ export async function modModifier(page: test.Page) {
return /\bMac OS\b/i.test(userAgent) ? 'Meta' : 'Control'
}
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
/**
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
export async function press(page: test.Page, keyOrShortcut: string) {
await test.test.step(`Press '${keyOrShortcut}'`, async () => {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
@ -837,8 +847,10 @@ export function mockAllAndLogin({ page, setupAPI }: MockParams) {
.do((thePage) => login({ page: thePage, setupAPI }))
}
/** Set up all mocks, and log in with dummy credentials.
* @deprecated Prefer {@link mockAllAndLogin}. */
/**
* Set up all mocks, and log in with dummy credentials.
* @deprecated Prefer {@link mockAllAndLogin}.
*/
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockAllAndLoginAndExposeAPI({ page, setupAPI }: MockParams) {

View File

@ -1,5 +1,5 @@
/** @file An action to open the User Menu. */
import { TEXT } from '../actions'
import { TEXT } from '.'
import type BaseActions from './BaseActions'
import type { PageCallback } from './BaseActions'

View File

@ -58,7 +58,7 @@ export interface SetupAPI {
}
/** The return type of {@link mockApi}. */
export interface MockApi extends Awaited<ReturnType<typeof mockApiInternal>> {}
export type MockApi = Awaited<ReturnType<typeof mockApiInternal>>
// This is a function, even though it does not contain function syntax.
// eslint-disable-next-line no-restricted-syntax
@ -582,7 +582,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
'Development' satisfies `${backend.VersionLifecycle.development}` as backend.VersionLifecycle.development,
value: '2023.2.1-dev',
},
// eslint-disable-next-line @typescript-eslint/naming-convention, no-restricted-syntax
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase, no-restricted-syntax
version_type: (new URL(request.url()).searchParams.get('version_type') ??
'') as backend.VersionType,
} satisfies backend.Version,
@ -603,9 +603,9 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
name: 'example project name',
state: project.projectState,
packageName: 'Project_root',
// eslint-disable-next-line @typescript-eslint/naming-convention
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
ide_version: null,
// eslint-disable-next-line @typescript-eslint/naming-convention
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
engine_version: {
value: '2023.2.1-nightly.2023.9.29',
lifecycle: backend.VersionLifecycle.development,

View File

@ -9,12 +9,7 @@ import type {
import { createContext, useContext, useEffect, useState } from 'react'
/** */
type ElementsContextValue_ = Parameters<Parameters<typeof StripeElementConsumer>[0]['children']>[0]
/** */
interface ElementsContextValue extends ElementsContextValue_ {
//
}
type ElementsContextValue = Parameters<Parameters<typeof StripeElementConsumer>[0]['children']>[0]
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ElementsContext = createContext<ElementsContextValue>(null!)

View File

@ -10,7 +10,7 @@ export const loadStripe = (): Promise<Stripe> =>
paymentMethod: {
id: '',
object: 'payment_method',
// eslint-disable-next-line @typescript-eslint/naming-convention
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
billing_details: {
address: null,
email: null,

View File

@ -46,7 +46,7 @@ test('Using breadcrumbs to navigate', async ({ page }) => {
await expectInsideMain(page)
// Breadcrumbs still have all the crumbs, but the last two are dimmed.
await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1', 'func2'])
await expect(locate.navBreadcrumb(page, (f) => f.class('inactive'))).toHaveText([
await expect(locate.navBreadcrumb(page).and(page.locator('.inactive'))).toHaveText([
'func1',
'func2',
])

Some files were not shown because too many files have changed in this diff Show More