@team-codeme/crud-forge 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/agents/generator-engineer.md +31 -0
- package/.cursor/agents/keycloak-engineer.md +33 -0
- package/.cursor/agents/qa-engineer.md +32 -0
- package/.cursor/agents/security-reviewer.md +30 -0
- package/.cursor/mcp.json +13 -0
- package/.cursor/rules/crudforge-precautions.mdc +16 -0
- package/.cursor/rules/playwright-browser.mdc +30 -0
- package/.cursor/rules/readme-todo-sync.mdc +13 -0
- package/.env.qa.example +19 -0
- package/.github/workflows/qa-live.yml +32 -0
- package/.github/workflows/qa.yml +27 -0
- package/.vscode/settings.json +32 -0
- package/QA/IAM-PLAN.md +25 -0
- package/QA/IAM-RESULTS.md +39 -0
- package/QA/IAM-TESTCASES.md +36 -0
- package/QA/IAM-TODO.md +7 -0
- package/QA/KEYCLOAK-BOOTSTRAP.md +105 -0
- package/QA/PLAN.md +43 -0
- package/QA/RESULTS.md +59 -0
- package/QA/TESTCASES.md +65 -0
- package/QA/TODO.md +14 -0
- package/QA/pmb-dev/README.md +49 -0
- package/QA/pmb-dev/config/config.yml +152 -0
- package/QA/pmb-dev/gdl/i.gdl +17 -0
- package/README.md +851 -2
- package/Todo.md +207 -0
- package/assets/branding/crudforge-logo.png +0 -0
- package/assets/branding/crudforge-mark.png +0 -0
- package/branding/poster-crudforge.jpg +0 -0
- package/chat/last-chat/Investigating Mock Data Configuration.md +428 -0
- package/cli.js +167 -0
- package/config/default-config.yml +245 -0
- package/docs/performance/README.md +217 -0
- package/docs/performance/after.json +111 -0
- package/docs/performance/before.json +111 -0
- package/docs/quality/README.md +276 -0
- package/e2e/crudforge.e2e.yml +47 -0
- package/e2e/crudforge.field-profile.yml +33 -0
- package/e2e/crudforge.iam-live.yml +124 -0
- package/e2e/crudforge.iam.yml +62 -0
- package/e2e/tests/field-profile.spec.js +152 -0
- package/e2e/tests/generated-crud.spec.js +251 -0
- package/e2e/tests/iam-live.spec.js +149 -0
- package/e2e/tests/iam-pack.spec.js +151 -0
- package/e2e/tests/list-state.spec.js +73 -0
- package/features.csv +27 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-mark.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/Dockerfile +14 -0
- package/fuse-starter-pack/fuse-v21.1/LICENSE.md +6 -0
- package/fuse-starter-pack/fuse-v21.1/README.md +59 -0
- package/fuse-starter-pack/fuse-v21.1/angular.json +113 -0
- package/fuse-starter-pack/fuse-v21.1/eslint.config.ts +150 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.conf +23 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.dev.conf +18 -0
- package/fuse-starter-pack/fuse-v21.1/package-lock.json +11513 -0
- package/fuse-starter-pack/fuse-v21.1/package.json +69 -0
- package/fuse-starter-pack/fuse-v21.1/postcss.config.json +5 -0
- package/fuse-starter-pack/fuse-v21.1/prettier.config.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.dev.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/favicon.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/en.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/es.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/ES.svg +79 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/US.svg +14 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/where-to-find-other-flags.txt +5 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo-mark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/push.sh +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.server.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.server.ts +8 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.ts +39 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/index.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/lucide.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/provider.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/local-storage.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/media.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/index.ts +3 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/models/theming.ts +13 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/palette.ts +209 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/provider.ts +24 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/theming.ts +129 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/transloco/transloco-http-loader.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/data/navigation.ts +176 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/layout.ts +77 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/language-switcher.ts +91 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/navigation.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/notifications.ts +252 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/scheme-switcher.ts +58 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/sidebar.ts +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/user.ts +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/academy.ts +753 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/model.ts +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/course.ts +318 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/courses.ts +228 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/routes.ts +14 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/contacts.ts +4372 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/model.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contact.ts +239 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contacts.ts +185 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/file-manager.ts +261 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/model.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/features/file-manager.ts +131 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/routes.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/ui/list.ts +164 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/faq.ts +288 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/guide.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/home.ts +219 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/support.ts +161 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/routes.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/model.ts +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/notes.ts +324 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/note.ts +152 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/notes.ts +118 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/model.ts +17 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/tasks.ts +562 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/task.ts +195 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/tasks.ts +271 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/analytics.ts +1620 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/finance.ts +1065 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/project.ts +376 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/analytics/analytics.ts +1096 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/finance/finance.ts +658 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/budget-table.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/project.ts +565 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/routes.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/changelog/changelog.ts +932 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/building.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/development.ts +42 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/installation.ts +47 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/routes.ts +45 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/features/error-page.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/routes.ts +21 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/features/notifications.ts +262 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/account.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/notifications.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/plan-and-billing.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/security.ts +211 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/team.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/layout.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/routes.ts +37 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/welcome/welcome.component.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/routes.ts +95 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.html +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.ts +51 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.html +82 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.ts +69 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.html +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.ts +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.html +183 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.ts +56 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/routes.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/features/coming-soon.ts +156 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/layout/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/features/home.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/routes.ts +30 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.mock.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.prod.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/index.html +66 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.server.ts +11 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/server.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/base.css +63 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/typography.css +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/a.css +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/input.css +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/kbd.css +55 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/material.css +1149 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/styles.css +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/theme.css +147 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/utilities.css +563 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/variants.css +4 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/third-party/lucide-icons.css +12 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.app.json +16 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.json +37 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.spec.json +10 -0
- package/generator/README.md +12 -0
- package/generator/issues/.gitkeep +0 -0
- package/package.json +56 -4
- package/playwright.config.js +29 -0
- package/playwright.field-profile.config.js +6 -0
- package/playwright.iam-live.config.js +31 -0
- package/playwright.iam.config.js +29 -0
- package/playwright.list-state.config.js +6 -0
- package/playwright.performance.config.js +1 -0
- package/run-config-tests.sh +5 -0
- package/scripts/e2e-paths.js +89 -0
- package/scripts/e2e-prepare-field-profile.js +17 -0
- package/scripts/e2e-prepare-iam-live.js +135 -0
- package/scripts/e2e-prepare-iam.js +65 -0
- package/scripts/e2e-prepare.js +72 -0
- package/scripts/e2e-serve-iam-live.js +28 -0
- package/scripts/e2e-serve-iam.js +28 -0
- package/scripts/e2e-serve.js +28 -0
- package/scripts/performance/generate.js +17 -0
- package/scripts/performance/measure.js +55 -0
- package/scripts/postinstall.js +9 -0
- package/scripts/prepare-quality-apps.js +34 -0
- package/scripts/run-config-tests.js +63 -0
- package/src/assets/logo-with-lable.jpeg +0 -0
- package/src/assets/logo.jpeg +0 -0
- package/src/commands/bootstrap.js +92 -0
- package/src/commands/doctor.js +403 -0
- package/src/commands/generate.js +301 -0
- package/src/commands/init-config.js +310 -0
- package/src/commands/keycloak-options.js +182 -0
- package/src/commands/validate.js +52 -0
- package/src/config/auth-config.js +183 -0
- package/src/config/default-values.js +235 -0
- package/src/config/env-refs.js +71 -0
- package/src/config/field-preferences.js +99 -0
- package/src/config/input-sources.js +104 -0
- package/src/config/load-config.js +166 -0
- package/src/config/proxy-routes.js +44 -0
- package/src/generator/access-control-tools.js +334 -0
- package/src/generator/ai-docs.js +691 -0
- package/src/generator/application-tests.js +252 -0
- package/src/generator/auth-pack.js +965 -0
- package/src/generator/embed-config.js +431 -0
- package/src/generator/entity-layout.js +30 -0
- package/src/generator/fuse-build.js +57 -0
- package/src/generator/fuse-runtime.js +344 -0
- package/src/generator/generated-banner.js +137 -0
- package/src/generator/index.js +874 -0
- package/src/generator/project-output.js +126 -0
- package/src/generator/team-pack.js +220 -0
- package/src/index.js +68 -0
- package/src/parser/gdl-parser.js +169 -0
- package/src/parser/jdl-parser.js +171 -0
- package/src/presets/pmb-test.js +139 -0
- package/src/presets/static-source.js +73 -0
- package/src/presets/store.js +96 -0
- package/src/provision/admin-service.js +148 -0
- package/src/provision/bootstrap.js +147 -0
- package/src/provision/http.js +123 -0
- package/src/provision/keycloak-admin-rest.js +335 -0
- package/src/provision/state.js +51 -0
- package/src/provision/token.js +52 -0
- package/src/templates/angular/auth/access-control.service.ts.ejs +179 -0
- package/src/templates/angular/auth/access-ctrl.pipe.ts.ejs +20 -0
- package/src/templates/angular/auth/auth.guard.ts.ejs +27 -0
- package/src/templates/angular/auth/auth.interceptor.ts.ejs +53 -0
- package/src/templates/angular/auth/auth.service.ts.ejs +594 -0
- package/src/templates/angular/auth/auth.utils.ts.ejs +28 -0
- package/src/templates/angular/auth/current-user.ts.ejs +24 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.css.ejs +95 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.html.ejs +33 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.ts.ejs +23 -0
- package/src/templates/angular/auth/fuse-account.ts.ejs +234 -0
- package/src/templates/angular/auth/fuse-user.ts.ejs +264 -0
- package/src/templates/angular/auth/has-permission.directive.ts.ejs +51 -0
- package/src/templates/angular/auth/index.ts.ejs +24 -0
- package/src/templates/angular/auth/keycloak-admin.service.ts.ejs +670 -0
- package/src/templates/angular/auth/landing.guard.ts.ejs +61 -0
- package/src/templates/angular/auth/no-auth.guard.ts.ejs +19 -0
- package/src/templates/angular/auth/permission-attribute.util.ts.ejs +182 -0
- package/src/templates/angular/auth/sign-in.ts.ejs +60 -0
- package/src/templates/angular/entity/_entity_.model.ts.ejs +64 -0
- package/src/templates/angular/entity/_entity_.routes.ts.ejs +9 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.html.ejs +168 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.ts.ejs +175 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.html.ejs +10 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.ts.ejs +86 -0
- package/src/templates/angular/entity/components/forms/update/_entity_-form.service.ts.ejs +136 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.html.ejs +68 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.ts.ejs +69 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.html.ejs +111 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.ts.ejs +102 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.css.ejs +33 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.html.ejs +29 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.ts.ejs +93 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.css.ejs +54 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.html.ejs +58 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.ts.ejs +47 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs +419 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.scss.ejs +48 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs +592 -0
- package/src/templates/angular/entity/service/_entity_.service.ts.ejs +153 -0
- package/src/templates/angular/entity/settings/_entity_.settings.ts.ejs +49 -0
- package/src/templates/angular/enums/_enum_.model.ts.ejs +11 -0
- package/src/templates/angular/iam/change-password.component.html.ejs +39 -0
- package/src/templates/angular/iam/change-password.component.ts.ejs +44 -0
- package/src/templates/angular/iam/no-access.component.ts.ejs +17 -0
- package/src/templates/angular/iam/role-list.component.html.ejs +331 -0
- package/src/templates/angular/iam/role-list.component.scss.ejs +107 -0
- package/src/templates/angular/iam/role-list.component.ts.ejs +597 -0
- package/src/templates/angular/iam/role-management/role-header.component.html.ejs +51 -0
- package/src/templates/angular/iam/role-management/role-header.component.ts.ejs +45 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.html.ejs +15 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.ts.ejs +19 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.css.ejs +24 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.html.ejs +108 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.ts.ejs +122 -0
- package/src/templates/angular/iam/role-management/role-users.component.html.ejs +104 -0
- package/src/templates/angular/iam/role-management/role-users.component.ts.ejs +53 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.html.ejs +113 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.ts.ejs +157 -0
- package/src/templates/angular/iam/role-permission-matrix.component.html.ejs +355 -0
- package/src/templates/angular/iam/role-permission-matrix.component.scss.ejs +217 -0
- package/src/templates/angular/iam/role-permission-matrix.component.ts.ejs +800 -0
- package/src/templates/angular/iam/screen-catalog.ts.ejs +147 -0
- package/src/templates/angular/iam/shared/confirm-dialog.component.ts.ejs +83 -0
- package/src/templates/angular/iam/shared/matrix-hierarchy.util.ts.ejs +196 -0
- package/src/templates/angular/iam/shared/permission-matrix-sound.ts.ejs +59 -0
- package/src/templates/angular/iam/shared/role-display.util.ts.ejs +43 -0
- package/src/templates/angular/iam/shared/role-permission-skeleton.component.ts.ejs +54 -0
- package/src/templates/angular/iam/shared/screen-matrix.util.ts.ejs +554 -0
- package/src/templates/angular/iam/shared/screen-navigation-map.ts.ejs +187 -0
- package/src/templates/angular/iam/shared/screen-navigation.registry.ts.ejs +64 -0
- package/src/templates/angular/iam/shared/text-highlight.pipe.ts.ejs +39 -0
- package/src/templates/angular/iam/user-access-matrix.component.html.ejs +212 -0
- package/src/templates/angular/iam/user-access-matrix.component.scss.ejs +34 -0
- package/src/templates/angular/iam/user-access-matrix.component.ts.ejs +239 -0
- package/src/templates/angular/iam/user-detail.component.html.ejs +143 -0
- package/src/templates/angular/iam/user-detail.component.ts.ejs +108 -0
- package/src/templates/angular/iam/user-form.component.html.ejs +63 -0
- package/src/templates/angular/iam/user-form.component.ts.ejs +100 -0
- package/src/templates/angular/iam/user-list.component.html.ejs +369 -0
- package/src/templates/angular/iam/user-list.component.scss.ejs +242 -0
- package/src/templates/angular/iam/user-list.component.ts.ejs +446 -0
- package/src/templates/angular/iam/user-management.routes.ts.ejs +47 -0
- package/src/templates/angular/iam/user-permission-matrix.component.html.ejs +231 -0
- package/src/templates/angular/iam/user-permission-matrix.component.scss.ejs +216 -0
- package/src/templates/angular/iam/user-permission-matrix.component.ts.ejs +516 -0
- package/src/templates/angular/shared/base/audit-user-name.pipe.ts.ejs +69 -0
- package/src/templates/angular/shared/base/base-filter.component.ts.ejs +245 -0
- package/src/templates/angular/shared/base/base-form.component.ts.ejs +109 -0
- package/src/templates/angular/shared/base/base-list.component.ts.ejs +507 -0
- package/src/templates/angular/shared/base/filter-types.ts.ejs +55 -0
- package/src/templates/angular/shared/base/input-format.directive.ts.ejs +47 -0
- package/src/templates/angular/shared/base/input-format.ts.ejs +15 -0
- package/src/templates/angular/shared/confirm-with-input-dialog.component.ts.ejs +91 -0
- package/src/templates/angular/shared/confirmation-dialog.component.ts.ejs +80 -0
- package/src/templates/angular/shared/list-styles.css.ejs +103 -0
- package/src/templates/angular/shared/loading-overlay.component.ts.ejs +24 -0
- package/src/templates/angular/shared/resizable.directive.ts.ejs +115 -0
- package/src/templates/angular/shared/snackbar-styles.css.ejs +11 -0
- package/src/templates/fuse-packs/access-control-tools/access-control-sync.js +819 -0
- package/src/templates/fuse-packs/access-control-tools/build-rename-map.js +67 -0
- package/src/templates/fuse-packs/access-control-tools/inject-qa-ids.js +14 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.js +628 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.test.js +323 -0
- package/src/templates/fuse-packs/access-control-tools/lib/html-scanner.js +362 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.duplicate.test.js +136 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.js +1095 -0
- package/src/templates/fuse-packs/access-control-tools/lib/ts-component-index.js +482 -0
- package/src/templates/fuse-packs/access-control-tools/output/.gitkeep +0 -0
- package/src/templates/fuse-packs/access-control-tools/run.sh +223 -0
- package/src/templates/fuse-packs/access-control-tools/scan-access-control.js +14 -0
- package/src/templates/fuse-packs/iam-assets/sound/apple-play.mp3 +0 -0
- package/src/templates/fuse-packs/iam-assets/sound/remove.mp3 +0 -0
- package/src/templates/generator/README.md +12 -0
- package/src/templates/testing/ci.cjs +45 -0
- package/src/templates/testing/mock.cjs +149 -0
- package/src/templates/testing/playwright.config.cjs +25 -0
- package/src/templates/testing/serve.cjs +22 -0
- package/src/templates/testing/smoke.spec.cjs +180 -0
- package/src/utils/branding.js +39 -0
- package/src/utils/colors.js +18 -0
- package/src/utils/diff.js +109 -0
- package/src/utils/merger.js +115 -0
- package/src/validator/backend-diagnostics.js +30 -0
- package/src/validator/blueprint-validator.js +276 -0
- package/src/validator/diagnostics.js +608 -0
- package/src/validator/pattern.js +28 -0
- package/test/auth-interceptor.js +84 -0
- package/test/auth-provision-checks.js +297 -0
- package/test/field-preferences.js +145 -0
- package/test/fixtures/field-profile.gdl +7 -0
- package/test/fixtures/mock.jdl +17 -0
- package/test/fixtures/mock.png +0 -0
- package/test/fixtures/performance/config.yml +33 -0
- package/test/fixtures/performance/large.gdl +199 -0
- package/test/fixtures/performance/small.gdl +7 -0
- package/test/fixtures/quality/custom.yml +22 -0
- package/test/fixtures/quality/go-duck.yml +22 -0
- package/test/fixtures/quality/jhipster.yml +30 -0
- package/test/fixtures/quality/representative.gdl +14 -0
- package/test/list-state.js +138 -0
- package/test/micro-functions-layout.js +95 -0
- package/test/performance-generation.js +32 -0
- package/test/pmb-preset.js +127 -0
- package/test/project-workflow.js +111 -0
- package/test/quality-features.js +250 -0
- package/test/verify-commands.js +83 -0
- package/test/verify.js +959 -0
- package/test-files/GDL/example.gdl +15 -0
- package/test-files/JDL/example.jdl +15 -0
- package/test-files/config/crudforge.config.yml +127 -0
- package/test-run.sh +272 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
project:
|
|
2
|
+
name: CRUDForge IAM Live
|
|
3
|
+
description: Generated Fuse app for live Keycloak QA (useMockAuth false)
|
|
4
|
+
|
|
5
|
+
output:
|
|
6
|
+
# Overridden by scripts/e2e-prepare*.js → $CRUDFORGE_TESTING_AREA/crudforge-app
|
|
7
|
+
directory: "./e2e/.app"
|
|
8
|
+
structure: "entity-folder"
|
|
9
|
+
overwritePolicy: "overwrite"
|
|
10
|
+
embedConfig: true
|
|
11
|
+
teamPack:
|
|
12
|
+
enabled: true
|
|
13
|
+
|
|
14
|
+
validation: true
|
|
15
|
+
|
|
16
|
+
fuse:
|
|
17
|
+
enabled: true
|
|
18
|
+
cleanLayout: true
|
|
19
|
+
includePrebuilt: false
|
|
20
|
+
folderType: ""
|
|
21
|
+
theme:
|
|
22
|
+
scheme: "light"
|
|
23
|
+
primary: "#0D1E3A"
|
|
24
|
+
error: "#b91c1c"
|
|
25
|
+
|
|
26
|
+
ui:
|
|
27
|
+
framework: "material"
|
|
28
|
+
viewMode: "table"
|
|
29
|
+
formMode: "sidebar"
|
|
30
|
+
searchMode: "field"
|
|
31
|
+
|
|
32
|
+
api:
|
|
33
|
+
baseUrl: "/api"
|
|
34
|
+
microservice: "book-service"
|
|
35
|
+
endpointStyle: "kebab-plural"
|
|
36
|
+
dateLibrary: "dayjs"
|
|
37
|
+
responseType: "http-response"
|
|
38
|
+
|
|
39
|
+
mock:
|
|
40
|
+
enabled: true
|
|
41
|
+
recordsPerEntity: 12
|
|
42
|
+
|
|
43
|
+
# Real Keycloak Admin Service (middleware) — keep in sync with test-run.sh + e2e-prepare-iam-live.js
|
|
44
|
+
# Docs: http://62.84.188.166:31740/ (do not point proxies at Keycloak :30080)
|
|
45
|
+
auth:
|
|
46
|
+
provider: keycloak
|
|
47
|
+
# Gateway for OIDC token via middleware /realms proxy (not native Keycloak :30080).
|
|
48
|
+
serverUrl: http://62.84.188.166:31740
|
|
49
|
+
realm: CRUD-FORDGE
|
|
50
|
+
clientId: crudforge-client-QA
|
|
51
|
+
issuerUrl: ""
|
|
52
|
+
tokenUrl: ""
|
|
53
|
+
adminBaseUrl: ""
|
|
54
|
+
useMockAuth: false
|
|
55
|
+
forwardedProto: https
|
|
56
|
+
# Prefills the Angular sign-in form (same as KEYCLOAK_ADMIN_* / operator).
|
|
57
|
+
signInPrefill:
|
|
58
|
+
username: crud
|
|
59
|
+
password: crud
|
|
60
|
+
adminRoles:
|
|
61
|
+
- ROLE_ADMIN
|
|
62
|
+
- ADMIN
|
|
63
|
+
- ROLE_SUPER_ADMIN
|
|
64
|
+
# Stay signed in by default; enable disabled-account overlay for live QA.
|
|
65
|
+
session:
|
|
66
|
+
idleTimeoutMs: 0
|
|
67
|
+
persistAcrossReload: true
|
|
68
|
+
crossTabSync: true
|
|
69
|
+
refreshOnFocus: true
|
|
70
|
+
statusProbeIntervalMs: 60000
|
|
71
|
+
disabledAccount:
|
|
72
|
+
enabled: true
|
|
73
|
+
countdownSeconds: 20
|
|
74
|
+
message: "Your account has been disabled. Please contact your administrator."
|
|
75
|
+
sound: false
|
|
76
|
+
provision:
|
|
77
|
+
enabled: true
|
|
78
|
+
adapter: admin-service
|
|
79
|
+
adminServiceUrl: http://62.84.188.166:31740/
|
|
80
|
+
# Resolve to crud / crud via test-run.sh pin + .env.qa
|
|
81
|
+
adminUsernameEnv: KEYCLOAK_ADMIN_USERNAME
|
|
82
|
+
adminPasswordEnv: KEYCLOAK_ADMIN_PASSWORD
|
|
83
|
+
tokenClientId: admin-cli
|
|
84
|
+
clientId: crudforge-client-QA
|
|
85
|
+
clientName: CRUDForge Client QA
|
|
86
|
+
publicClient: true
|
|
87
|
+
directAccessGrantsEnabled: true
|
|
88
|
+
redirectUris:
|
|
89
|
+
- http://127.0.0.1:4202/*
|
|
90
|
+
- http://localhost:4202/*
|
|
91
|
+
webOrigins:
|
|
92
|
+
- http://127.0.0.1:4202
|
|
93
|
+
- http://localhost:4202
|
|
94
|
+
- "+"
|
|
95
|
+
roles:
|
|
96
|
+
- ROLE_ADMIN
|
|
97
|
+
- ROLE_CRUDFORGE_QA_USER
|
|
98
|
+
users:
|
|
99
|
+
- username: crudforge-qa-admin
|
|
100
|
+
passwordEnv: CRUDFORGE_QA_ADMIN_PASSWORD
|
|
101
|
+
email: crudforge-qa-admin@example.com
|
|
102
|
+
firstName: QA
|
|
103
|
+
lastName: Admin
|
|
104
|
+
enabled: true
|
|
105
|
+
temporaryPassword: false
|
|
106
|
+
roles:
|
|
107
|
+
- ROLE_ADMIN
|
|
108
|
+
- username: crudforge-qa-user
|
|
109
|
+
passwordEnv: CRUDFORGE_QA_USER_PASSWORD
|
|
110
|
+
email: crudforge-qa-user@example.com
|
|
111
|
+
firstName: QA
|
|
112
|
+
lastName: User
|
|
113
|
+
enabled: true
|
|
114
|
+
temporaryPassword: false
|
|
115
|
+
roles:
|
|
116
|
+
- ROLE_CRUDFORGE_QA_USER
|
|
117
|
+
resetExistingPasswords: false
|
|
118
|
+
stateFile: ".crudforge/provision-state.json"
|
|
119
|
+
|
|
120
|
+
iam:
|
|
121
|
+
enabled: true
|
|
122
|
+
|
|
123
|
+
proxy:
|
|
124
|
+
write: both
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
project:
|
|
2
|
+
name: CRUDForge IAM E2E
|
|
3
|
+
description: Generated Fuse app used to QA the Keycloak auth + IAM pack
|
|
4
|
+
|
|
5
|
+
output:
|
|
6
|
+
# Overridden by scripts/e2e-prepare*.js → $CRUDFORGE_TESTING_AREA/crudforge-app
|
|
7
|
+
directory: "./e2e/.app"
|
|
8
|
+
structure: "entity-folder"
|
|
9
|
+
overwritePolicy: "overwrite"
|
|
10
|
+
embedConfig: true
|
|
11
|
+
teamPack:
|
|
12
|
+
enabled: true
|
|
13
|
+
|
|
14
|
+
validation: true
|
|
15
|
+
|
|
16
|
+
fuse:
|
|
17
|
+
enabled: true
|
|
18
|
+
cleanLayout: true
|
|
19
|
+
includePrebuilt: false
|
|
20
|
+
folderType: ""
|
|
21
|
+
theme:
|
|
22
|
+
scheme: "light"
|
|
23
|
+
primary: "#0f766e"
|
|
24
|
+
error: "#b91c1c"
|
|
25
|
+
|
|
26
|
+
ui:
|
|
27
|
+
framework: "material"
|
|
28
|
+
viewMode: "table"
|
|
29
|
+
formMode: "sidebar"
|
|
30
|
+
searchMode: "field"
|
|
31
|
+
snackbarPositionTop: false
|
|
32
|
+
|
|
33
|
+
api:
|
|
34
|
+
baseUrl: "/api"
|
|
35
|
+
microservice: "book-service"
|
|
36
|
+
endpointStyle: "kebab-plural"
|
|
37
|
+
dateLibrary: "dayjs"
|
|
38
|
+
responseType: "http-response"
|
|
39
|
+
|
|
40
|
+
mock:
|
|
41
|
+
enabled: true
|
|
42
|
+
recordsPerEntity: 12
|
|
43
|
+
|
|
44
|
+
auth:
|
|
45
|
+
provider: keycloak
|
|
46
|
+
realm: crudforge
|
|
47
|
+
clientId: crudforge-admin
|
|
48
|
+
adminBaseUrl: /auth
|
|
49
|
+
useMockAuth: true
|
|
50
|
+
adminRoles:
|
|
51
|
+
- ROLE_ADMIN
|
|
52
|
+
- ADMIN
|
|
53
|
+
- ROLE_SUPER_ADMIN
|
|
54
|
+
|
|
55
|
+
iam:
|
|
56
|
+
enabled: true
|
|
57
|
+
users: true
|
|
58
|
+
roles: true
|
|
59
|
+
rolePermissions: true
|
|
60
|
+
userAccess: true
|
|
61
|
+
userPermissions: true
|
|
62
|
+
inheritance: true
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const { test, expect } = require('@playwright/test');
|
|
2
|
+
|
|
3
|
+
// This suite uses a fresh PMB-preset app with Province code=uppercase/name=lowercase.
|
|
4
|
+
// Every authentication/API request is intercepted: no real credentials or backend writes.
|
|
5
|
+
async function setup(page, testInfo) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
testInfo.browserDiagnostics = diagnostics;
|
|
8
|
+
page.on('pageerror', error => diagnostics.push({type:'pageerror',message:error.message,stack:error.stack}));
|
|
9
|
+
page.on('console', message => { if(message.type()==='error') diagnostics.push({type:'console',message:message.text(),location:message.location()}); });
|
|
10
|
+
let username = 'alice';
|
|
11
|
+
const profiles = {
|
|
12
|
+
alice: {sub:'alice-id',name:'Alice Example',email:'alice@example.test',preferred_username:'alice',picture:'/test-avatar.svg'},
|
|
13
|
+
bob: {sub:'bob-id',preferred_username:'bob',email:'bob@example.test'},
|
|
14
|
+
};
|
|
15
|
+
let payload;
|
|
16
|
+
const requests = [];
|
|
17
|
+
const row={id:1,index:'Keep This',provinceCode:'ab',provinceName:'Mixed Name',isActive:true,createdBy:'audit-user',createdDate:'2026-01-01T00:00:00Z'};
|
|
18
|
+
let rows = Array.from({length:12},(_,index)=>({...row,id:index+1}));
|
|
19
|
+
await page.route('**/*', async route => {
|
|
20
|
+
const request=route.request(), url=new URL(request.url());
|
|
21
|
+
if(url.pathname.endsWith('/token')) {
|
|
22
|
+
const params=new URLSearchParams(request.postData());
|
|
23
|
+
username=params.get('username') || username;
|
|
24
|
+
const claims={...profiles[username],exp:Math.floor(Date.now()/1000)+3600,realm_access:{roles:['ROLE_ADMIN']}};
|
|
25
|
+
const token='test.'+Buffer.from(JSON.stringify(claims)).toString('base64url')+'.test';
|
|
26
|
+
return route.fulfill({json:{access_token:token,refresh_token:'test-refresh',expires_in:3600,token_type:'Bearer'}});
|
|
27
|
+
}
|
|
28
|
+
if(url.pathname.endsWith('/logout')) return route.fulfill({status:204});
|
|
29
|
+
if(url.pathname==='/test-avatar.svg') return route.fulfill({contentType:'image/svg+xml',body:'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32" fill="blue"/></svg>'});
|
|
30
|
+
if(url.pathname.includes('/services/keycloak-admin/')) return route.fulfill({json:{id:profiles[username].sub,attributes:{province:['VIEW','CREATE','UPDATE']}}});
|
|
31
|
+
if(url.pathname.includes('/services/pmb-reference-module/api/provinces')) {
|
|
32
|
+
requests.push({method:request.method(),path:url.pathname,params:Object.fromEntries(url.searchParams)});
|
|
33
|
+
if(request.method()==='POST') {payload=request.postDataJSON();const created={...payload,id:rows.length+1};rows.push(created);return route.fulfill({json:created});}
|
|
34
|
+
if(request.method()==='PUT') {payload=request.postDataJSON();rows=rows.map(row=>row.id===payload.id?payload:row);return route.fulfill({json:payload});}
|
|
35
|
+
if(request.method()==='DELETE') {rows=rows.filter(row=>row.id!==Number(url.pathname.split('/').pop()));return route.fulfill({status:204});}
|
|
36
|
+
const pageIndex=Number(url.searchParams.get('page')||0),size=Number(url.searchParams.get('size')||10);
|
|
37
|
+
return route.fulfill({headers:{'X-Total-Count':String(rows.length)},json:/\/provinces\/\d+$/.test(url.pathname)?rows.find(row=>row.id===Number(url.pathname.split('/').pop())):rows.slice(pageIndex*size,(pageIndex+1)*size)});
|
|
38
|
+
}
|
|
39
|
+
if(url.pathname.startsWith('/services/') || url.pathname.startsWith('/realms/')) return route.fulfill({status:404,json:{message:'Unmocked test endpoint'}});
|
|
40
|
+
if(url.hostname!=='127.0.0.1' && url.hostname!=='localhost') return route.abort();
|
|
41
|
+
return route.continue();
|
|
42
|
+
});
|
|
43
|
+
async function login(name='alice') {
|
|
44
|
+
await page.goto('/auth/sign-in');
|
|
45
|
+
await page.waitForFunction(() => { const element = document.querySelector('auth-sign-in'); return !!element && !!window.ng?.getComponent(element); });
|
|
46
|
+
await page.getByLabel('Username or email').fill(name);
|
|
47
|
+
await page.getByLabel('Password', {exact:true}).fill('test-only');
|
|
48
|
+
await page.getByRole('button',{name:'Sign in',exact:true}).click();
|
|
49
|
+
await expect(page.locator('user').first()).toContainText(name==='alice'?'Alice Example':'bob');
|
|
50
|
+
}
|
|
51
|
+
return { login, requests, payload:()=>payload, async verifyDiagnostics() {
|
|
52
|
+
await testInfo.attach('browser-diagnostics.json',{body:JSON.stringify(diagnostics,null,2),contentType:'application/json'});
|
|
53
|
+
expect(diagnostics).toEqual([]);
|
|
54
|
+
}};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('authenticated identity survives refresh and clears when switching accounts',async({page},testInfo)=>{
|
|
58
|
+
const app=await setup(page,testInfo);
|
|
59
|
+
await app.login();
|
|
60
|
+
const user=page.locator('user').first();
|
|
61
|
+
await expect(user).toContainText('alice@example.test');
|
|
62
|
+
await expect(user.locator('img')).toHaveAttribute('src','/test-avatar.svg');
|
|
63
|
+
await user.locator('button').first().click();
|
|
64
|
+
await expect(page.getByRole('menu')).toContainText('Alice Example');
|
|
65
|
+
await page.keyboard.press('Escape');
|
|
66
|
+
await page.reload();
|
|
67
|
+
await expect(user).toContainText('Alice Example');
|
|
68
|
+
await user.locator('button').first().click();
|
|
69
|
+
await page.getByRole('menuitem',{name:'Sign out',exact:true}).click();
|
|
70
|
+
await expect(page).toHaveURL(/\/auth\/sign-in/);
|
|
71
|
+
expect(await page.evaluate(()=>Object.keys(localStorage).filter(key=>/accessToken|refreshToken/.test(key)))).toEqual([]);
|
|
72
|
+
await app.login('bob');
|
|
73
|
+
await expect(user).toContainText('bob@example.test');
|
|
74
|
+
await expect(user).not.toContainText('Alice');
|
|
75
|
+
await expect(user.locator('[aria-label="User initials"]')).toHaveText('B');
|
|
76
|
+
await page.reload();
|
|
77
|
+
await expect(user).toContainText('bob@example.test');
|
|
78
|
+
await expect(page.locator('body')).not.toContainText('CRUDForge User');
|
|
79
|
+
await app.verifyDiagnostics();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('audit inputs are absent and case formatting reaches form values and API payloads',async({page},testInfo)=>{
|
|
83
|
+
const app=await setup(page,testInfo);await app.login();
|
|
84
|
+
await page.goto('/admin/province');
|
|
85
|
+
await expect(page.getByRole('heading',{name:'Province Management'})).toBeVisible();
|
|
86
|
+
await page.getByRole('button',{name:'Open filters'}).click();
|
|
87
|
+
const filters=page.locator('app-province-filter');
|
|
88
|
+
for(const label of ['Created By','Created Date','Last Modified By','Last Modified Date']) await expect(filters).not.toContainText(label);
|
|
89
|
+
await filters.locator('button').filter({has:page.locator('mat-icon[svgIcon="x"]')}).click();
|
|
90
|
+
await page.locator('app-province-actions mat-select').click();
|
|
91
|
+
for(const label of ['Created By','Created Date','Last Modified By','Last Modified Date']) await expect(page.getByRole('option',{name:label,exact:true})).toHaveCount(0);
|
|
92
|
+
await page.keyboard.press('Escape');
|
|
93
|
+
await page.getByRole('button',{name:'New',exact:true}).click();
|
|
94
|
+
const form=page.locator('app-province-form');
|
|
95
|
+
for(const name of ['createdBy','createdDate','lastModifiedBy','lastModifiedDate']) await expect(form.locator(`[formControlName="${name}"]`)).toHaveCount(0);
|
|
96
|
+
const code=form.locator('[formControlName="provinceCode"]');
|
|
97
|
+
await code.pressSequentially('hello world');await expect(code).toHaveValue('HELLO WORLD');
|
|
98
|
+
await code.evaluate(el=>el.setSelectionRange(5,5));await code.pressSequentially('x');
|
|
99
|
+
await expect(code).toHaveValue('HELLOX WORLD');expect(await code.evaluate(el=>el.selectionStart)).toBe(6);
|
|
100
|
+
// Insert text through the browser input pipeline, like a paste into the current selection.
|
|
101
|
+
await code.selectText();await page.keyboard.insertText('pasted text');await expect(code).toHaveValue('PASTED TEXT');
|
|
102
|
+
await form.locator('[formControlName="provinceName"]').fill('HELLO WORLD');
|
|
103
|
+
await form.locator('[formControlName="index"]').fill('Keep Case');
|
|
104
|
+
await expect(form.locator('[formControlName="provinceName"]')).toHaveValue('hello world');
|
|
105
|
+
await form.getByRole('button',{name:'Save',exact:true}).click();
|
|
106
|
+
await expect.poll(()=>app.payload()?.provinceCode).toBe('PASTED TEXT');
|
|
107
|
+
expect(app.payload().provinceName).toBe('hello world');expect(app.payload().index).toBe('Keep Case');
|
|
108
|
+
await page.locator('button[matTooltip="Edit Province"]').first().click();
|
|
109
|
+
await page.getByRole('button',{name:'Confirm',exact:true}).click();
|
|
110
|
+
await expect(code).toHaveValue('AB');await expect(form.locator('[formControlName="provinceName"]')).toHaveValue('mixed name');
|
|
111
|
+
await form.getByRole('button',{name:'Save',exact:true}).click();
|
|
112
|
+
await expect.poll(()=>app.payload()?.provinceCode).toBe('AB');
|
|
113
|
+
expect(app.payload().createdBy).toBe('audit-user');
|
|
114
|
+
await app.verifyDiagnostics();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test.afterEach(async ({}, testInfo) => {
|
|
118
|
+
await testInfo.attach('complete-browser-diagnostics.json', {body:JSON.stringify(testInfo.browserDiagnostics || [],null,2),contentType:'application/json'});
|
|
119
|
+
if(testInfo.status !== testInfo.expectedStatus) console.log(JSON.stringify(testInfo.browserDiagnostics || [],null,2));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
test('pagination issues one request, resizing persists, and delete refreshes once', async ({page},testInfo) => {
|
|
124
|
+
const app=await setup(page,testInfo); await app.login();
|
|
125
|
+
await page.goto('/admin/province');
|
|
126
|
+
await expect(page.locator('td.mat-column-provinceCode').first()).toBeVisible();
|
|
127
|
+
const reads=()=>app.requests.filter(r=>r.method==='GET' && r.path.endsWith('/provinces')).length;
|
|
128
|
+
const before=reads();
|
|
129
|
+
await page.getByRole('button',{name:'Next page'}).click();
|
|
130
|
+
await expect(page.locator('td.mat-column-id').first()).toHaveText('11');
|
|
131
|
+
expect(reads()-before).toBe(1);
|
|
132
|
+
expect(app.requests.at(-1).params.size).toBe('10');
|
|
133
|
+
await page.getByRole('button',{name:'Previous page'}).click();
|
|
134
|
+
await expect(page.locator('td.mat-column-id').first()).toHaveText('1');
|
|
135
|
+
const header=page.locator('th.mat-column-provinceCode');
|
|
136
|
+
const handle=header.locator('.resize-handle');
|
|
137
|
+
const bounds=await handle.boundingBox();const original=(await header.boundingBox()).width;
|
|
138
|
+
await page.mouse.move(bounds.x+bounds.width/2,bounds.y+bounds.height/2);await page.mouse.down();
|
|
139
|
+
await page.mouse.move(bounds.x+bounds.width/2+70,bounds.y+bounds.height/2);await page.mouse.up();
|
|
140
|
+
const saved=await page.evaluate(()=>Number(localStorage.getItem('province-provinceCode-width')));
|
|
141
|
+
expect(saved).toBeGreaterThan(original+40);
|
|
142
|
+
await page.reload();await expect(header).toBeVisible();
|
|
143
|
+
expect((await header.boundingBox()).width).toBeCloseTo(saved,0);
|
|
144
|
+
await page.locator('button[matTooltip="Delete Province"]').first().click();
|
|
145
|
+
const dialog=page.getByRole('dialog');await expect(dialog).toBeVisible();
|
|
146
|
+
await dialog.locator('input').fill('1');
|
|
147
|
+
const prior=reads();await dialog.getByRole('button',{name:'Confirm',exact:true}).click();
|
|
148
|
+
await expect(page.locator('td.mat-column-id').first()).toHaveText('2');
|
|
149
|
+
expect(app.requests.filter(r=>r.method==='DELETE')).toHaveLength(1);
|
|
150
|
+
expect(reads()-prior).toBe(1);
|
|
151
|
+
await app.verifyDiagnostics();
|
|
152
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const { test, expect } = require('@playwright/test');
|
|
2
|
+
|
|
3
|
+
async function signIn(page) {
|
|
4
|
+
await page.goto('/auth/sign-in');
|
|
5
|
+
await expect(page.getByText('Sign in', { exact: true }).first()).toBeVisible();
|
|
6
|
+
const signInButton = page.getByRole('button', { name: 'Sign in' });
|
|
7
|
+
await expect(signInButton).toBeEnabled();
|
|
8
|
+
await Promise.all([
|
|
9
|
+
page.waitForURL(/\/admin(\/|$)/, { timeout: 20_000 }),
|
|
10
|
+
signInButton.click(),
|
|
11
|
+
]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function openBooks(page) {
|
|
15
|
+
await signIn(page);
|
|
16
|
+
await page.goto('/admin/book');
|
|
17
|
+
await expect(page.getByRole('heading', { name: /Book Management/i })).toBeVisible();
|
|
18
|
+
await expect(page.getByRole('cell', { name: 'Mock title 1', exact: true })).toBeVisible();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function confirmIfPresent(page) {
|
|
22
|
+
const confirm = page.getByRole('button', { name: 'Confirm' });
|
|
23
|
+
if (await confirm.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
24
|
+
await confirm.click();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test.describe('AUTH', () => {
|
|
29
|
+
test('AUTH-01 Sign in with demo credentials', async ({ page }) => {
|
|
30
|
+
await signIn(page);
|
|
31
|
+
await expect(page).toHaveURL(/\/admin/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('AUTH-02 Sign-in branding and theme button', async ({ page }) => {
|
|
35
|
+
await page.goto('/auth/sign-in');
|
|
36
|
+
await expect(page.getByAltText('CRUDForge logo')).toBeVisible();
|
|
37
|
+
await expect(page.getByText('Welcome to CRUDForge')).toBeVisible();
|
|
38
|
+
const signInButton = page.getByRole('button', { name: 'Sign in' });
|
|
39
|
+
await expect(signInButton).toBeVisible();
|
|
40
|
+
const colorMatch = await signInButton.evaluate((el) => {
|
|
41
|
+
const background = getComputedStyle(el).backgroundColor;
|
|
42
|
+
const primary = getComputedStyle(document.documentElement)
|
|
43
|
+
.getPropertyValue('--theme-color-primary-600')
|
|
44
|
+
.trim();
|
|
45
|
+
const ctx = document.createElement('canvas').getContext('2d');
|
|
46
|
+
ctx.fillStyle = background;
|
|
47
|
+
const normalizedBackground = ctx.fillStyle;
|
|
48
|
+
ctx.fillStyle = primary;
|
|
49
|
+
return {
|
|
50
|
+
background,
|
|
51
|
+
primary,
|
|
52
|
+
matches: normalizedBackground === ctx.fillStyle,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
expect(colorMatch.primary).toBeTruthy();
|
|
56
|
+
expect(colorMatch.matches, `button ${colorMatch.background} vs theme ${colorMatch.primary}`).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test.describe('NAV', () => {
|
|
61
|
+
test('NAV-01 Open Books', async ({ page }) => {
|
|
62
|
+
await openBooks(page);
|
|
63
|
+
await expect(page.getByRole('button', { name: 'New' })).toBeVisible();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('NAV-02 Open Authors', async ({ page }) => {
|
|
67
|
+
await signIn(page);
|
|
68
|
+
await page.goto('/admin/author');
|
|
69
|
+
await expect(page.getByRole('heading', { name: /Author Management/i })).toBeVisible();
|
|
70
|
+
await expect(page.getByRole('button', { name: 'New' })).toBeVisible();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test.describe('LIST', () => {
|
|
75
|
+
test('LIST-01 Mock rows render', async ({ page }) => {
|
|
76
|
+
await openBooks(page);
|
|
77
|
+
await expect(page.getByText(/Mock title/i).first()).toBeVisible();
|
|
78
|
+
await expect(page.getByText(/of 12/i)).toBeVisible();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('LIST-02 Pagination', async ({ page }) => {
|
|
82
|
+
await openBooks(page);
|
|
83
|
+
const next = page.getByRole('button', { name: /Next page/i });
|
|
84
|
+
await expect(next).toBeVisible();
|
|
85
|
+
await next.click();
|
|
86
|
+
await expect(page.getByText(/11 – 12 of 12|11 - 12 of 12/)).toBeVisible();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('LIST-03 Pin row updates label', async ({ page }) => {
|
|
90
|
+
await openBooks(page);
|
|
91
|
+
const pin = page.getByRole('button', { name: 'Pin row' }).first();
|
|
92
|
+
await pin.click();
|
|
93
|
+
await expect(page.getByRole('button', { name: 'Unpin row' }).first()).toBeVisible();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('LIST-04 Flag row applies color class', async ({ page }) => {
|
|
97
|
+
await openBooks(page);
|
|
98
|
+
await page.getByRole('button', { name: 'Set color flag' }).first().click();
|
|
99
|
+
await page.getByRole('menuitem', { name: 'Blue' }).click();
|
|
100
|
+
await expect(page.locator('tr.has-flag').first()).toBeVisible();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('LIST-05 Export menu lists filtered and all', async ({ page }) => {
|
|
104
|
+
await openBooks(page);
|
|
105
|
+
await expect(page.getByRole('cell', { name: 'Mock title 1', exact: true })).toBeVisible();
|
|
106
|
+
const exportButton = page.getByRole('button', { name: 'Export options' });
|
|
107
|
+
await expect(exportButton).toBeEnabled();
|
|
108
|
+
await exportButton.click();
|
|
109
|
+
await expect(page.getByRole('menuitem', { name: 'Export Filtered Data' })).toBeVisible();
|
|
110
|
+
await expect(page.getByRole('menuitem', { name: 'Export All Data' })).toBeVisible();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test.describe('SEARCH', () => {
|
|
115
|
+
test('SEARCH-01 Field search controls', async ({ page }) => {
|
|
116
|
+
await openBooks(page);
|
|
117
|
+
await expect(page.getByPlaceholder('Search…')).toBeVisible();
|
|
118
|
+
await expect(page.getByRole('combobox').first()).toBeVisible();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('SEARCH-02 Search by title filters mock rows', async ({ page }) => {
|
|
122
|
+
await openBooks(page);
|
|
123
|
+
await page.getByPlaceholder('Search…').fill('Mock title 1');
|
|
124
|
+
await expect(page.getByRole('cell', { name: 'Mock title 1', exact: true })).toBeVisible();
|
|
125
|
+
await expect(page.getByRole('cell', { name: 'Mock title 2', exact: true })).toHaveCount(0);
|
|
126
|
+
await expect(page.getByText(/of 4/i)).toBeVisible();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test.describe('FILTER', () => {
|
|
131
|
+
test('FILTER-01 Open filter drawer', async ({ page }) => {
|
|
132
|
+
await openBooks(page);
|
|
133
|
+
await page.getByRole('button', { name: 'Open filters' }).click();
|
|
134
|
+
await expect(page.getByRole('heading', { name: 'Advanced Filters' })).toBeVisible();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('FILTER-02 Apply title contains filter', async ({ page }) => {
|
|
138
|
+
await openBooks(page);
|
|
139
|
+
await page.getByRole('button', { name: 'Open filters' }).click();
|
|
140
|
+
const titleSection = page.locator('div.rounded-2xl').filter({
|
|
141
|
+
has: page.getByRole('heading', { name: 'Title', exact: true }),
|
|
142
|
+
});
|
|
143
|
+
await titleSection.getByRole('combobox').click();
|
|
144
|
+
await page.getByRole('option', { name: 'Contains' }).click();
|
|
145
|
+
await titleSection.getByRole('textbox').fill('Mock title 1');
|
|
146
|
+
await page.getByRole('button', { name: 'Apply Filters' }).click();
|
|
147
|
+
await expect(page.getByRole('heading', { name: 'Advanced Filters' })).toBeHidden();
|
|
148
|
+
await expect(page.getByRole('cell', { name: 'Mock title 1', exact: true })).toBeVisible();
|
|
149
|
+
await expect(page.getByRole('cell', { name: 'Mock title 2', exact: true })).toHaveCount(0);
|
|
150
|
+
await expect(page.getByText(/of 4/i)).toBeVisible();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test.describe('FORM', () => {
|
|
155
|
+
test('FORM-01 New opens sidebar', async ({ page }) => {
|
|
156
|
+
await openBooks(page);
|
|
157
|
+
await page.getByRole('button', { name: 'New' }).click();
|
|
158
|
+
await expect(page).not.toHaveURL(/\/book\/new$/);
|
|
159
|
+
const drawer = page.locator('mat-drawer.cforge-form-drawer');
|
|
160
|
+
await expect(drawer).toBeVisible();
|
|
161
|
+
await expect(drawer.getByRole('heading', { name: 'New Book' })).toBeVisible();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('FORM-02 Edit opens sidebar', async ({ page }) => {
|
|
165
|
+
await openBooks(page);
|
|
166
|
+
await page.locator('button[matTooltip="Edit Book"]').first().click();
|
|
167
|
+
await confirmIfPresent(page);
|
|
168
|
+
await expect(page).not.toHaveURL(/\/book\/\d+\/edit$/);
|
|
169
|
+
const drawer = page.locator('mat-drawer.cforge-form-drawer');
|
|
170
|
+
await expect(drawer).toBeVisible();
|
|
171
|
+
await expect(drawer.getByRole('heading', { name: 'Edit Book' })).toBeVisible();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('FORM-03 New validation blocks empty required title', async ({ page }) => {
|
|
175
|
+
await openBooks(page);
|
|
176
|
+
await page.getByRole('button', { name: 'New' }).click();
|
|
177
|
+
const drawer = page.locator('mat-drawer.cforge-form-drawer');
|
|
178
|
+
await expect(drawer.getByRole('heading', { name: 'New Book' })).toBeVisible();
|
|
179
|
+
await expect(drawer.getByRole('button', { name: 'Save' })).toBeDisabled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('FORM-04 New cancel closes drawer', async ({ page }) => {
|
|
183
|
+
await openBooks(page);
|
|
184
|
+
await page.getByRole('button', { name: 'New' }).click();
|
|
185
|
+
const drawer = page.locator('mat-drawer.cforge-form-drawer');
|
|
186
|
+
await drawer.getByRole('button', { name: 'Cancel' }).click();
|
|
187
|
+
await expect(drawer).toBeHidden();
|
|
188
|
+
await expect(page).toHaveURL(/\/admin\/book\/?$/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('FORM-05 Author new sidebar', async ({ page }) => {
|
|
192
|
+
await signIn(page);
|
|
193
|
+
await page.goto('/admin/author');
|
|
194
|
+
await page.getByRole('button', { name: 'New' }).click();
|
|
195
|
+
await expect(page).not.toHaveURL(/\/author\/new$/);
|
|
196
|
+
const drawer = page.locator('mat-drawer.cforge-form-drawer');
|
|
197
|
+
await expect(drawer).toBeVisible();
|
|
198
|
+
await expect(drawer.getByRole('heading', { name: 'New Author' })).toBeVisible();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test.describe('DETAIL', () => {
|
|
203
|
+
test('DETAIL-01 View details', async ({ page }) => {
|
|
204
|
+
await openBooks(page);
|
|
205
|
+
await page.locator('button[matTooltip="View Details"]').first().click();
|
|
206
|
+
await expect(page).toHaveURL(/\/admin\/book\/\d+\/view/);
|
|
207
|
+
await expect(page.getByRole('heading', { name: 'Book Details' })).toBeVisible();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('DETAIL-02 Back from detail', async ({ page }) => {
|
|
211
|
+
await openBooks(page);
|
|
212
|
+
await page.locator('button[matTooltip="View Details"]').first().click();
|
|
213
|
+
await page.getByRole('button', { name: 'Back' }).click();
|
|
214
|
+
await expect(page.getByRole('heading', { name: /Book Management/i })).toBeVisible();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test.describe('DELETE', () => {
|
|
219
|
+
test('DELETE-01 Delete confirm cancel keeps row', async ({ page }) => {
|
|
220
|
+
await openBooks(page);
|
|
221
|
+
await page.locator('button[matTooltip="Delete Book"]').first().click();
|
|
222
|
+
await expect(page.getByRole('heading', { name: 'Delete Book' })).toBeVisible();
|
|
223
|
+
await page.getByRole('button', { name: 'Cancel' }).click();
|
|
224
|
+
await expect(page.getByText('Mock title 1').first()).toBeVisible();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test.describe('CARD', () => {
|
|
229
|
+
test('CARD-01 Toggle card view and edit from card', async ({ page }) => {
|
|
230
|
+
await openBooks(page);
|
|
231
|
+
await page.getByRole('button', { name: 'Switch to Card View' }).click();
|
|
232
|
+
await expect(page.getByRole('button', { name: 'Edit' }).first()).toBeVisible();
|
|
233
|
+
await page.getByRole('button', { name: 'Edit' }).first().click();
|
|
234
|
+
await confirmIfPresent(page);
|
|
235
|
+
await expect(page).not.toHaveURL(/\/book\/\d+\/edit$/);
|
|
236
|
+
await expect(page.locator('mat-drawer.cforge-form-drawer')).toBeVisible();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('RELATIONSHIP lazy collection and create dialogs open', async ({ page }) => {
|
|
241
|
+
await signIn(page);
|
|
242
|
+
await page.goto('/admin/author');
|
|
243
|
+
await expect(page.getByRole('heading', { name: 'Author Management' })).toBeVisible();
|
|
244
|
+
await page.locator('button[matTooltip="Show Book"]').first().click();
|
|
245
|
+
const dialog = page.getByRole('dialog');
|
|
246
|
+
await expect(dialog.getByRole('heading', { name: 'Book Management' })).toBeVisible();
|
|
247
|
+
await page.keyboard.press('Escape');
|
|
248
|
+
await expect(dialog).toBeHidden();
|
|
249
|
+
await page.locator('button[matTooltip="Add Book"]').first().click();
|
|
250
|
+
await expect(page.getByRole('dialog').getByRole('heading', { name: 'New Book for Author' })).toBeVisible();
|
|
251
|
+
});
|