mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 02:21:54 +03:00
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:
parent
204b37c6c3
commit
4a249688e8
31
.github/CODEOWNERS
vendored
31
.github/CODEOWNERS
vendored
@ -19,14 +19,30 @@ Cargo.toml
|
|||||||
/lib/rust/parser/ @farmaazon @kazcw @vitvakatu @Frizi @jaroslavtulach @AdRiley
|
/lib/rust/parser/ @farmaazon @kazcw @vitvakatu @Frizi @jaroslavtulach @AdRiley
|
||||||
/tools/build-performance/ @kazcw @Akirathan
|
/tools/build-performance/ @kazcw @Akirathan
|
||||||
|
|
||||||
|
# Global JS configuration
|
||||||
|
esling.config.mjs
|
||||||
|
tsconfig.json
|
||||||
|
|
||||||
# Scala Libraries
|
# Scala Libraries
|
||||||
/lib/scala/ @4e6 @jaroslavtulach @hubertp @Akirathan
|
/lib/scala/ @4e6 @jaroslavtulach @hubertp @Akirathan
|
||||||
|
|
||||||
# Java libraries
|
# Java libraries
|
||||||
/lib/java/ @4e6 @jaroslavtulach @hubertp @Akirathan
|
/lib/java/ @4e6 @jaroslavtulach @hubertp @Akirathan
|
||||||
|
|
||||||
# GUI
|
# GUI/Dashboard
|
||||||
/app/gui2/ @Frizi @farmaazon @vitvakatu @kazcw @AdRiley
|
/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)
|
# Engine (old)
|
||||||
# This section should be removed once the engine moves to /app/engine
|
# 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
|
# The default project template is owned by the libraries team
|
||||||
/lib/scala/pkg/src/main/resources/default/src/ @radeusgd @jdunkerley @GregoryTravis @AdRiley @marthasharkey
|
/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
|
|
||||||
|
@ -39,10 +39,10 @@ app/ide-desktop/lib/dashboard/playwright/.cache/
|
|||||||
app/ide-desktop/lib/dashboard/dist/
|
app/ide-desktop/lib/dashboard/dist/
|
||||||
app/gui/view/documentation/assets/stylesheet.css
|
app/gui/view/documentation/assets/stylesheet.css
|
||||||
app/rust-ffi/pkg
|
app/rust-ffi/pkg
|
||||||
app/gui2/src/assets/font-*.css
|
app/gui/src/project-view/assets/font-*.css
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
build.json
|
build.json
|
||||||
app/gui2/playwright-report/
|
app/gui/playwright-report/
|
||||||
|
|
||||||
# Engine Builds can leave these nested working copies.
|
# Engine Builds can leave these nested working copies.
|
||||||
# TODO [mwu]: Adjust Engine build to not leave them.
|
# TODO [mwu]: Adjust Engine build to not leave them.
|
||||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"eslint.experimental.useFlatConfig": true,
|
"eslint.useFlatConfig": true,
|
||||||
"eslint.useESLintClass": true,
|
"eslint.useESLintClass": true,
|
||||||
"[javascript][typescript][typescriptreact][vue]": {
|
"[javascript][typescript][typescriptreact][vue]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
14
README.md
14
README.md
@ -21,7 +21,7 @@
|
|||||||
<img src="https://img.shields.io/static/v1?label=Compiler%20License&message=Apache%20v2&color=2ec352&labelColor=2c3239"
|
<img src="https://img.shields.io/static/v1?label=Compiler%20License&message=Apache%20v2&color=2ec352&labelColor=2c3239"
|
||||||
alt="License">
|
alt="License">
|
||||||
</a>
|
</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"
|
<img src="https://img.shields.io/static/v1?label=GUI%20License&message=AGPL%20v3&color=2ec352&labelColor=2c3239"
|
||||||
alt="License">
|
alt="License">
|
||||||
</a>
|
</a>
|
||||||
@ -207,11 +207,11 @@ Enso consists of several sub projects:
|
|||||||
command line tools.
|
command line tools.
|
||||||
|
|
||||||
- **Enso IDE:** The
|
- **Enso IDE:** The
|
||||||
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a
|
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui) is a desktop
|
||||||
desktop application that allows working with the visual form of Enso. It
|
application that allows working with the visual form of Enso. It consists of
|
||||||
consists of an Electron application, a high performance WebGL UI framework,
|
an Electron application, a high performance WebGL UI framework, and the
|
||||||
and the searcher which provides contextual search, hints, and documentation
|
searcher which provides contextual search, hints, and documentation for all of
|
||||||
for all of Enso's functionality.
|
Enso's functionality.
|
||||||
|
|
||||||
<br/>
|
<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
|
[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),
|
IDE is licensed under the [AGPL 3.0](https://opensource.org/licenses/AGPL-3.0),
|
||||||
as specified in the
|
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,
|
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
|
create libraries, and release them under any license of your choice, while also
|
||||||
|
10
app/.vscode/launch.json
vendored
10
app/.vscode/launch.json
vendored
@ -46,7 +46,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "GUI (Storybook)",
|
"name": "GUI (Storybook)",
|
||||||
"runtimeExecutable": "pnpm",
|
"runtimeExecutable": "pnpm",
|
||||||
"runtimeArgs": ["run", "--filter", "enso-gui2", "story:dev"],
|
"runtimeArgs": ["run", "--filter", "enso-gui", "story:dev"],
|
||||||
"outputCapture": "std"
|
"outputCapture": "std"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -70,7 +70,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "GUI (E2E UI)",
|
"name": "GUI (E2E UI)",
|
||||||
"runtimeExecutable": "pnpm",
|
"runtimeExecutable": "pnpm",
|
||||||
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:e2e", "--", "--ui"],
|
"runtimeArgs": ["run", "--filter", "enso-gui", "test:e2e", "--", "--ui"],
|
||||||
"outputCapture": "std"
|
"outputCapture": "std"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -102,14 +102,14 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "GUI (All tests)",
|
"name": "GUI (All tests)",
|
||||||
"runtimeExecutable": "pnpm",
|
"runtimeExecutable": "pnpm",
|
||||||
"runtimeArgs": ["run", "--filter", "enso-gui2", "test"]
|
"runtimeArgs": ["run", "--filter", "enso-gui", "test"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "GUI (E2E tests)",
|
"name": "GUI (E2E tests)",
|
||||||
"runtimeExecutable": "pnpm",
|
"runtimeExecutable": "pnpm",
|
||||||
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:e2e"],
|
"runtimeArgs": ["run", "--filter", "enso-gui", "test:e2e"],
|
||||||
"outputCapture": "std"
|
"outputCapture": "std"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -117,7 +117,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "GUI (Unit tests)",
|
"name": "GUI (Unit tests)",
|
||||||
"runtimeExecutable": "pnpm",
|
"runtimeExecutable": "pnpm",
|
||||||
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:unit", "--", "run"],
|
"runtimeArgs": ["run", "--filter", "enso-gui", "test:unit", "--", "run"],
|
||||||
"outputCapture": "std"
|
"outputCapture": "std"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.js",
|
".": "./src/index.js",
|
||||||
"./src/config.json": "./src/config.json",
|
"./src/config.json": "./src/config.json",
|
||||||
|
"./src/accessToken": "./src/accessToken.ts",
|
||||||
"./src/appConfig": "./src/appConfig.js",
|
"./src/appConfig": "./src/appConfig.js",
|
||||||
"./src/buildUtils": "./src/buildUtils.js",
|
"./src/buildUtils": "./src/buildUtils.js",
|
||||||
"./src/detect": "./src/detect.ts",
|
"./src/detect": "./src/detect.ts",
|
@ -445,8 +445,7 @@ export interface CheckoutSessionStatus {
|
|||||||
/** Status of the payment for the checkout session. */
|
/** Status of the payment for the checkout session. */
|
||||||
readonly paymentStatus: string
|
readonly paymentStatus: string
|
||||||
/** Status of the checkout session. */
|
/** Status of the checkout session. */
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
readonly status: 'active' | 'trialing' | (string & NonNullable<unknown>)
|
||||||
readonly status: 'active' | 'trialing' | (string & {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resource usage of a VM. */
|
/** Resource usage of a VM. */
|
@ -1,4 +0,0 @@
|
|||||||
playwright-report/
|
|
||||||
playwright/.cache/
|
|
||||||
test-results/
|
|
||||||
dist/
|
|
@ -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>
|
|
@ -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 |
@ -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_"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
@ -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: {},
|
|
||||||
},
|
|
||||||
}
|
|
@ -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 |
@ -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(),
|
|
||||||
})
|
|
@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
12
app/gui2/.gitignore → app/gui/.gitignore
vendored
12
app/gui2/.gitignore → app/gui/.gitignore
vendored
@ -8,6 +8,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
|
mockDist
|
||||||
*.local
|
*.local
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
@ -25,12 +26,11 @@ coverage
|
|||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|
||||||
src/util/iconList.json
|
|
||||||
src/util/iconName.ts
|
|
||||||
src/stores/visualization/metadata.json
|
|
||||||
public/font-dejavu/
|
public/font-dejavu/
|
||||||
public/font-enso/
|
public/font-enso/
|
||||||
public/font-mplus1/
|
public/font-mplus1/
|
||||||
src/assets/font-dejavu.css
|
src/project-view/assets/font-dejavu.css
|
||||||
src/assets/font-enso.css
|
src/project-view/assets/font-enso.css
|
||||||
src/assets/font-mplus1.css
|
src/project-view/assets/font-mplus1.css
|
||||||
|
src/project-view/util/iconList.json
|
||||||
|
src/project-view/util/iconName.ts
|
@ -3,7 +3,7 @@ import * as test from '@playwright/test'
|
|||||||
|
|
||||||
import type * as inputBindings from '#/utilities/inputBindings'
|
import type * as inputBindings from '#/utilities/inputBindings'
|
||||||
|
|
||||||
import { modModifier } from '../actions'
|
import { modModifier } from '.'
|
||||||
|
|
||||||
// ====================
|
// ====================
|
||||||
// === PageCallback ===
|
// === PageCallback ===
|
||||||
@ -27,7 +27,8 @@ export interface LocatorCallback {
|
|||||||
// === BaseActions ===
|
// === 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.
|
* It contains method common to all `Actions` subclasses.
|
||||||
* This is a [`thenable`], so it can be used as if it was a {@link Promise}.
|
* 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(),
|
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]() {
|
get [Symbol.toStringTag]() {
|
||||||
return this.constructor.name
|
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> {
|
static press(page: test.Page, keyOrShortcut: string): Promise<void> {
|
||||||
return test.test.step(`Press '${keyOrShortcut}'`, async () => {
|
return test.test.step(`Press '${keyOrShortcut}'`, async () => {
|
||||||
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
|
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)
|
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
|
* 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`.
|
// The following types are copied almost verbatim from the type definitions for `Promise`.
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
async catch<T>(onrejected?: ((reason: unknown) => PromiseLike<T> | T) | null | undefined) {
|
async catch<T>(onrejected?: ((reason: unknown) => PromiseLike<T> | T) | null | undefined) {
|
||||||
return await this.promise.catch(onrejected)
|
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
|
* 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> {
|
async finally(onfinally?: (() => void) | null | undefined): Promise<void> {
|
||||||
await this.promise.finally(onfinally)
|
await this.promise.finally(onfinally)
|
||||||
}
|
}
|
||||||
@ -101,9 +110,11 @@ export default class BaseActions implements Promise<void> {
|
|||||||
return new clazz(this.page, this.promise, ...args)
|
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
|
* 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 {
|
do(callback: PageCallback): this {
|
||||||
// @ts-expect-error This is SAFE, but only when the constructor of this class has the exact
|
// @ts-expect-error This is SAFE, but only when the constructor of this class has the exact
|
||||||
// same parameters as `BaseActions`.
|
// 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)))
|
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>) {
|
press<Key extends string>(keyOrShortcut: inputBindings.AutocompleteKeybind<Key>) {
|
||||||
return this.do((page) => BaseActions.press(page, keyOrShortcut))
|
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) {
|
expectInputError(testId: string, description: string, expected: string | null | undefined) {
|
||||||
if (expected === undefined) {
|
if (expected === undefined) {
|
||||||
return this
|
return this
|
@ -12,7 +12,7 @@ import {
|
|||||||
locateSecretNameInput,
|
locateSecretNameInput,
|
||||||
locateSecretValueInput,
|
locateSecretValueInput,
|
||||||
TEXT,
|
TEXT,
|
||||||
} from '../actions'
|
} from '.'
|
||||||
import type * as baseActions from './BaseActions'
|
import type * as baseActions from './BaseActions'
|
||||||
import * as contextMenuActions from './contextMenuActions'
|
import * as contextMenuActions from './contextMenuActions'
|
||||||
import EditorPageActions from './EditorPageActions'
|
import EditorPageActions from './EditorPageActions'
|
||||||
@ -120,8 +120,10 @@ export default class DrivePageActions extends PageActions {
|
|||||||
locateAssetRows(page).nth(index).click({ position: ASSET_ROW_SAFE_POSITION }),
|
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) {
|
rightClickRow(index: number) {
|
||||||
return self.step(`Right click drive table row #${index}`, (page) =>
|
return self.step(`Right click drive table row #${index}`, (page) =>
|
||||||
locateAssetRows(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() {
|
expectPlaceholderRow() {
|
||||||
return self.step('Expect placeholder row', async (page) => {
|
return self.step('Expect placeholder row', async (page) => {
|
||||||
await test.expect(locateAssetRows(page)).toHaveCount(0)
|
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/)
|
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() {
|
expectTrashPlaceholderRow() {
|
||||||
return self.step('Expect trash placeholder row', async (page) => {
|
return self.step('Expect trash placeholder row', async (page) => {
|
||||||
await test.expect(locateAssetRows(page)).toHaveCount(0)
|
await test.expect(locateAssetRows(page)).toHaveCount(0)
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Available actions for the login page. */
|
/** @file Available actions for the login page. */
|
||||||
import * as test from '@playwright/test'
|
import * as test from '@playwright/test'
|
||||||
|
|
||||||
import { TEXT, VALID_EMAIL } from '../actions'
|
import { TEXT, VALID_EMAIL } from '.'
|
||||||
import BaseActions, { type LocatorCallback } from './BaseActions'
|
import BaseActions, { type LocatorCallback } from './BaseActions'
|
||||||
import LoginPageActions from './LoginPageActions'
|
import LoginPageActions from './LoginPageActions'
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Available actions for the login page. */
|
/** @file Available actions for the login page. */
|
||||||
import * as test from '@playwright/test'
|
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 BaseActions, { type LocatorCallback } from './BaseActions'
|
||||||
import DrivePageActions from './DrivePageActions'
|
import DrivePageActions from './DrivePageActions'
|
||||||
import ForgotPasswordPageActions from './ForgotPasswordPageActions'
|
import ForgotPasswordPageActions from './ForgotPasswordPageActions'
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Actions for a "new Data Link" modal. */
|
/** @file Actions for a "new Data Link" modal. */
|
||||||
import type * as test from 'playwright/test'
|
import type * as test from 'playwright/test'
|
||||||
|
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import type * as baseActions from './BaseActions'
|
import type * as baseActions from './BaseActions'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import DrivePageActions from './DrivePageActions'
|
import DrivePageActions from './DrivePageActions'
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Available actions for the login page. */
|
/** @file Available actions for the login page. */
|
||||||
import * as test from '@playwright/test'
|
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 BaseActions, { type LocatorCallback } from './BaseActions'
|
||||||
import LoginPageActions from './LoginPageActions'
|
import LoginPageActions from './LoginPageActions'
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Actions for the fourth step of the "setup" page. */
|
/** @file Actions for the fourth step of the "setup" page. */
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import DrivePageActions from './DrivePageActions'
|
import DrivePageActions from './DrivePageActions'
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Actions for the third step of the "setup" page. */
|
/** @file Actions for the third step of the "setup" page. */
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import SetupTeamPageActions from './SetupTeamPageActions'
|
import SetupTeamPageActions from './SetupTeamPageActions'
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Actions for the third step of the "setup" page. */
|
/** @file Actions for the third step of the "setup" page. */
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import SetupInvitePageActions from './SetupInvitePageActions'
|
import SetupInvitePageActions from './SetupInvitePageActions'
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Actions for the second step of the "setup" page. */
|
/** @file Actions for the second step of the "setup" page. */
|
||||||
import { PLAN_TO_UPGRADE_LABEL_ID } from '#/modules/payments/constants'
|
import { PLAN_TO_UPGRADE_LABEL_ID } from '#/modules/payments/constants'
|
||||||
import { Plan } from 'enso-common/src/services/Backend'
|
import { Plan } from 'enso-common/src/services/Backend'
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import SetupDonePageActions from './SetupDonePageActions'
|
import SetupDonePageActions from './SetupDonePageActions'
|
||||||
import SetupOrganizationPageActions from './SetupOrganizationPageActions'
|
import SetupOrganizationPageActions from './SetupOrganizationPageActions'
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Actions for the "setup" page. */
|
/** @file Actions for the "setup" page. */
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import SetupDonePageActions from './SetupDonePageActions'
|
import SetupDonePageActions from './SetupDonePageActions'
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Actions for the "setup" page. */
|
/** @file Actions for the "setup" page. */
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import SetupPlanPageActions from './SetupPlanPageActions'
|
import SetupPlanPageActions from './SetupPlanPageActions'
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Actions for the "home" page. */
|
/** @file Actions for the "home" page. */
|
||||||
import * as actions from '../actions'
|
import * as actions from '.'
|
||||||
import BaseActions from './BaseActions'
|
import BaseActions from './BaseActions'
|
||||||
import DrivePageActions from './DrivePageActions'
|
import DrivePageActions from './DrivePageActions'
|
||||||
import EditorPageActions from './EditorPageActions'
|
import EditorPageActions from './EditorPageActions'
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Actions for the context menu. */
|
/** @file Actions for the context menu. */
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import type * as baseActions from './BaseActions'
|
import type * as baseActions from './BaseActions'
|
||||||
import type BaseActions from './BaseActions'
|
import type BaseActions from './BaseActions'
|
||||||
import EditorPageActions from './EditorPageActions'
|
import EditorPageActions from './EditorPageActions'
|
@ -4,9 +4,9 @@ import * as test from '@playwright/test'
|
|||||||
|
|
||||||
import { TEXTS } from 'enso-common/src/text'
|
import { TEXTS } from 'enso-common/src/text'
|
||||||
|
|
||||||
import DrivePageActions from './actions/DrivePageActions'
|
import * as apiModule from '../api'
|
||||||
import LoginPageActions from './actions/LoginPageActions'
|
import DrivePageActions from './DrivePageActions'
|
||||||
import * as apiModule from './api'
|
import LoginPageActions from './LoginPageActions'
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
/* 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. */
|
/** Find an editor container (if any) on the current page. */
|
||||||
export function locateEditor(page: test.Page) {
|
export function locateEditor(page: test.Page) {
|
||||||
// Test ID of a placeholder editor component used during testing.
|
// 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. */
|
/** 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)')
|
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) {
|
export function locateExpandableDirectories(page: test.Page) {
|
||||||
// The icon is hidden when not hovered so `getByLabel` will not work.
|
// The icon is hidden when not hovered so `getByLabel` will not work.
|
||||||
// eslint-disable-next-line no-restricted-properties
|
// eslint-disable-next-line no-restricted-properties
|
||||||
return locateAssetRows(page).filter({ has: page.locator('[aria-label=Expand]') })
|
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) {
|
export function locateCollapsibleDirectories(page: test.Page) {
|
||||||
// The icon is hidden when not hovered so `getByLabel` will not work.
|
// The icon is hidden when not hovered so `getByLabel` will not work.
|
||||||
// eslint-disable-next-line no-restricted-properties
|
// eslint-disable-next-line no-restricted-properties
|
||||||
@ -390,9 +394,11 @@ export function locateExtraColumns(page: test.Page) {
|
|||||||
return page.getByTestId('extra-columns')
|
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
|
* 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) {
|
export function locateRootDirectoryDropzone(page: test.Page) {
|
||||||
// This has no identifying features.
|
// This has no identifying features.
|
||||||
return page.getByTestId('root-directory-dropzone')
|
return page.getByTestId('root-directory-dropzone')
|
||||||
@ -591,9 +597,11 @@ export namespace settings {
|
|||||||
// === Visual layout utilities ===
|
// === 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
|
* 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) {
|
export function getAssetRowLeftPx(locator: test.Locator) {
|
||||||
return locator.evaluate((el) => el.children[0]?.children[0]?.getBoundingClientRect().left ?? 0)
|
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'
|
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) {
|
export async function press(page: test.Page, keyOrShortcut: string) {
|
||||||
await test.test.step(`Press '${keyOrShortcut}'`, async () => {
|
await test.test.step(`Press '${keyOrShortcut}'`, async () => {
|
||||||
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
|
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
|
||||||
@ -837,8 +847,10 @@ export function mockAllAndLogin({ page, setupAPI }: MockParams) {
|
|||||||
.do((thePage) => login({ page: thePage, setupAPI }))
|
.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.
|
// This syntax is required for Playwright to work properly.
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
export async function mockAllAndLoginAndExposeAPI({ page, setupAPI }: MockParams) {
|
export async function mockAllAndLoginAndExposeAPI({ page, setupAPI }: MockParams) {
|
@ -1,5 +1,5 @@
|
|||||||
/** @file An action to open the User Menu. */
|
/** @file An action to open the User Menu. */
|
||||||
import { TEXT } from '../actions'
|
import { TEXT } from '.'
|
||||||
import type BaseActions from './BaseActions'
|
import type BaseActions from './BaseActions'
|
||||||
import type { PageCallback } from './BaseActions'
|
import type { PageCallback } from './BaseActions'
|
||||||
|
|
@ -58,7 +58,7 @@ export interface SetupAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The return type of {@link mockApi}. */
|
/** 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.
|
// This is a function, even though it does not contain function syntax.
|
||||||
// eslint-disable-next-line no-restricted-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,
|
'Development' satisfies `${backend.VersionLifecycle.development}` as backend.VersionLifecycle.development,
|
||||||
value: '2023.2.1-dev',
|
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') ??
|
version_type: (new URL(request.url()).searchParams.get('version_type') ??
|
||||||
'') as backend.VersionType,
|
'') as backend.VersionType,
|
||||||
} satisfies backend.Version,
|
} satisfies backend.Version,
|
||||||
@ -603,9 +603,9 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
|
|||||||
name: 'example project name',
|
name: 'example project name',
|
||||||
state: project.projectState,
|
state: project.projectState,
|
||||||
packageName: 'Project_root',
|
packageName: 'Project_root',
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
ide_version: null,
|
ide_version: null,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
engine_version: {
|
engine_version: {
|
||||||
value: '2023.2.1-nightly.2023.9.29',
|
value: '2023.2.1-nightly.2023.9.29',
|
||||||
lifecycle: backend.VersionLifecycle.development,
|
lifecycle: backend.VersionLifecycle.development,
|
@ -9,12 +9,7 @@ import type {
|
|||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
type ElementsContextValue_ = Parameters<Parameters<typeof StripeElementConsumer>[0]['children']>[0]
|
type ElementsContextValue = Parameters<Parameters<typeof StripeElementConsumer>[0]['children']>[0]
|
||||||
|
|
||||||
/** */
|
|
||||||
interface ElementsContextValue extends ElementsContextValue_ {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const ElementsContext = createContext<ElementsContextValue>(null!)
|
const ElementsContext = createContext<ElementsContextValue>(null!)
|
@ -10,7 +10,7 @@ export const loadStripe = (): Promise<Stripe> =>
|
|||||||
paymentMethod: {
|
paymentMethod: {
|
||||||
id: '',
|
id: '',
|
||||||
object: 'payment_method',
|
object: 'payment_method',
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
billing_details: {
|
billing_details: {
|
||||||
address: null,
|
address: null,
|
||||||
email: null,
|
email: null,
|
@ -46,7 +46,7 @@ test('Using breadcrumbs to navigate', async ({ page }) => {
|
|||||||
await expectInsideMain(page)
|
await expectInsideMain(page)
|
||||||
// Breadcrumbs still have all the crumbs, but the last two are dimmed.
|
// 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)).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',
|
'func1',
|
||||||
'func2',
|
'func2',
|
||||||
])
|
])
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user