@cosmicdrift/kumiko-framework 0.387.0 → 0.389.0
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/AGENTS.md +9 -1
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +206 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +299 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +101 -0
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +61 -2
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
|
@@ -34,6 +34,698 @@ bun test
|
|
|
34
34
|
|
|
35
35
|
## Source files
|
|
36
36
|
|
|
37
|
+
### e2e/build-server.ts
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// Tiny Bun server that builds and serves the e2e bundle — same shape as
|
|
41
|
+
// writeform-section/e2e/build-server.ts, radically stripped down (no
|
|
42
|
+
// Postgres, no auth). The MockDispatcher (fixtures/mock-dispatcher.ts)
|
|
43
|
+
// replaces the whole stack.
|
|
44
|
+
//
|
|
45
|
+
// The AppSchema is NOT duplicated in the client bundle (drift risk against
|
|
46
|
+
// the real feature code). Instead this server process builds the schema
|
|
47
|
+
// from the real src/feature.ts via createRegistry + buildAppSchema (both
|
|
48
|
+
// pure, no DB/HTTP) and serves it as JSON over /schema.json — client.tsx
|
|
49
|
+
// fetches it on boot.
|
|
50
|
+
//
|
|
51
|
+
// Port 4190: 4172-4189 are taken by other samples/apps (see their
|
|
52
|
+
// playwright.config.ts/package.json), 4190 is free.
|
|
53
|
+
//
|
|
54
|
+
// /styles.css is REALLY compiled from Tailwind (not stubbed) — this recipe
|
|
55
|
+
// exists to prove a real box-model fix (fw#2778: a relatedList tab panel
|
|
56
|
+
// must size to its content, not stretch to fill). Without real CSS the
|
|
57
|
+
// getBoundingClientRect() measurements in height.spec.ts would be against
|
|
58
|
+
// unstyled browser default flow, not the actual flex chain under test.
|
|
59
|
+
// Entry stylesheet = fixtures/styles.css, a local wrapper that re-exports
|
|
60
|
+
// renderer/web's styles.css and adds an @source covering this e2e dir.
|
|
61
|
+
//
|
|
62
|
+
// One-shot build, no watcher (the server only runs for the test duration)
|
|
63
|
+
// — dies hard on a Tailwind CLI/build failure instead of degrading to an
|
|
64
|
+
// empty stylesheet and letting the layout assertions pass vacuously.
|
|
65
|
+
|
|
66
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
67
|
+
import { tmpdir } from "node:os";
|
|
68
|
+
import { join, resolve } from "node:path";
|
|
69
|
+
import { buildAppSchema, createRegistry } from "@cosmicdrift/kumiko-framework/engine";
|
|
70
|
+
import {
|
|
71
|
+
canResolveTailwindStylesheet,
|
|
72
|
+
resolveTailwindCli,
|
|
73
|
+
} from "@cosmicdrift/kumiko-server-runtime/resolve-tailwind-cli";
|
|
74
|
+
import { createOrderDeskFeature } from "../src/feature";
|
|
75
|
+
|
|
76
|
+
const HERE = resolve(import.meta.dir);
|
|
77
|
+
const ENTRY = resolve(HERE, "fixtures/client.tsx");
|
|
78
|
+
const HTML_PATH = resolve(HERE, "fixtures/index.html");
|
|
79
|
+
|
|
80
|
+
async function buildStylesheet(): Promise<string> {
|
|
81
|
+
const cliPath = resolveTailwindCli({ bun: Bun, cwd: HERE });
|
|
82
|
+
if (cliPath === undefined) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"record-detail-layout/e2e: @tailwindcss/cli not resolvable — run `bun install` at the repo root.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const entryCss = resolve(HERE, "fixtures/styles.css");
|
|
88
|
+
if (!canResolveTailwindStylesheet(entryCss, { bun: Bun, cwd: HERE })) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`record-detail-layout/e2e: tailwindcss not resolvable for ${entryCss} — peer dependency missing at the stylesheet's location.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const outDir = mkdtempSync(join(tmpdir(), "record-detail-layout-e2e-tw-"));
|
|
94
|
+
try {
|
|
95
|
+
const outPath = join(outDir, "styles.css");
|
|
96
|
+
const bunBin = process.argv[0] ?? "bun";
|
|
97
|
+
const build = Bun.spawnSync([bunBin, "run", cliPath, "-i", entryCss, "-o", outPath], {
|
|
98
|
+
cwd: HERE,
|
|
99
|
+
});
|
|
100
|
+
if (!build.success) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`record-detail-layout/e2e: tailwind build failed (exit ${build.exitCode})\n${build.stderr.toString()}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return await Bun.file(outPath).text();
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const css = await buildStylesheet();
|
|
112
|
+
|
|
113
|
+
const port = Number(process.env["PORT"] ?? 4190);
|
|
114
|
+
|
|
115
|
+
const registry = createRegistry([createOrderDeskFeature()]);
|
|
116
|
+
const schema = buildAppSchema(registry);
|
|
117
|
+
const schemaJson = JSON.stringify(schema);
|
|
118
|
+
|
|
119
|
+
const built = await Bun.build({
|
|
120
|
+
entrypoints: [ENTRY],
|
|
121
|
+
target: "browser",
|
|
122
|
+
sourcemap: "linked",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!built.success) {
|
|
126
|
+
// biome-ignore lint/suspicious/noConsole: e2e build script — no logger wired
|
|
127
|
+
for (const log of built.logs) console.error(log);
|
|
128
|
+
throw new Error("record-detail-layout/e2e: client bundle failed");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const jsOutput = built.outputs.find((o) => o.path.endsWith(".js"));
|
|
132
|
+
const mapOutput = built.outputs.find((o) => o.path.endsWith(".js.map"));
|
|
133
|
+
if (!jsOutput) throw new Error("record-detail-layout/e2e: bundle has no .js output");
|
|
134
|
+
|
|
135
|
+
const js = await jsOutput.text();
|
|
136
|
+
const map = mapOutput ? await mapOutput.text() : "";
|
|
137
|
+
const html = await Bun.file(HTML_PATH).text();
|
|
138
|
+
|
|
139
|
+
Bun.serve({
|
|
140
|
+
port,
|
|
141
|
+
fetch(req: Request): Response {
|
|
142
|
+
const url = new URL(req.url);
|
|
143
|
+
if (url.pathname === "/client.js") {
|
|
144
|
+
return new Response(js, {
|
|
145
|
+
headers: { "Content-Type": "application/javascript; charset=utf-8" },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (url.pathname === "/client.js.map") {
|
|
149
|
+
return new Response(map, { headers: { "Content-Type": "application/json; charset=utf-8" } });
|
|
150
|
+
}
|
|
151
|
+
if (url.pathname === "/schema.json") {
|
|
152
|
+
return new Response(schemaJson, {
|
|
153
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (url.pathname === "/styles.css") {
|
|
157
|
+
return new Response(css, { headers: { "Content-Type": "text/css; charset=utf-8" } });
|
|
158
|
+
}
|
|
159
|
+
if (url.pathname === "/api/sse" || url.pathname.startsWith("/sse")) {
|
|
160
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
161
|
+
start(controller) {
|
|
162
|
+
controller.enqueue(new TextEncoder().encode(": connected\n\n"));
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return new Response(stream, {
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "text/event-stream",
|
|
168
|
+
"Cache-Control": "no-cache",
|
|
169
|
+
Connection: "keep-alive",
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// biome-ignore lint/suspicious/noConsole: e2e build script — no logger wired
|
|
178
|
+
console.log(`record-detail-layout/e2e build-server listening on http://localhost:${port}`);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### e2e/fixtures/client.tsx
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// Browser entry for record-detail-layout/e2e (fw#2778). Fetches the
|
|
185
|
+
// server-built AppSchema (built from the real src/feature.ts, see
|
|
186
|
+
// build-server.ts) and mounts createKumikoApp with the MockDispatcher.
|
|
187
|
+
//
|
|
188
|
+
// Pattern copied from writeform-section/e2e/fixtures/client.tsx: the shell
|
|
189
|
+
// renders its own explicitly-keyed KumikoScreen (entityId="order-1") instead
|
|
190
|
+
// of createKumikoApp's routed `children` — that's the only way to pin a
|
|
191
|
+
// projectionDetail screen's entityId without forking the URL-routing
|
|
192
|
+
// internals. `screenQn` is still required (createKumikoApp throws without a
|
|
193
|
+
// resolvable initial route), but the routed content it produces is unused.
|
|
194
|
+
//
|
|
195
|
+
// DefaultAppShell wraps the screen in the same viewport-height chrome
|
|
196
|
+
// (`h-svh` shell, `main` scrolls internally) a real app gives it — the
|
|
197
|
+
// fillHeight/scrollBody chain under test only resolves against a genuinely
|
|
198
|
+
// height-constrained ancestor, not document-flow. `user` grants the
|
|
199
|
+
// "Admin" role order-detail's screen access requires.
|
|
200
|
+
//
|
|
201
|
+
// order-detail is a projectionDetail screen in `layout.mode: "tabs"` — the
|
|
202
|
+
// first tab ("items", a relatedList section) is active by default (no `tab`
|
|
203
|
+
// search param), which is exactly the fw#2778 scenario: a lone relatedList
|
|
204
|
+
// tab that must size to its content when short and scroll internally when
|
|
205
|
+
// long, instead of stretching the tab panel to the bottom.
|
|
206
|
+
|
|
207
|
+
import type { AppSchema } from "@cosmicdrift/kumiko-renderer";
|
|
208
|
+
import { ExtensionSectionsProvider } from "@cosmicdrift/kumiko-renderer";
|
|
209
|
+
import { createKumikoApp, DefaultAppShell, KumikoScreen } from "@cosmicdrift/kumiko-renderer/web";
|
|
210
|
+
import type { ReactNode } from "react";
|
|
211
|
+
import { createMockDispatcher } from "./mock-dispatcher";
|
|
212
|
+
|
|
213
|
+
const ORDER_DETAIL_SCREEN_QN = "order-desk:screen:order-detail";
|
|
214
|
+
|
|
215
|
+
// Plugin-mounted "extension" tab (fw#3234 padding-parity screenshots) — no
|
|
216
|
+
// own Card/Section, exactly what guard-no-framed-extension-sections
|
|
217
|
+
// enforces: the host's own tabs-mode Card is the only frame this gets.
|
|
218
|
+
function OrderInternalNote(): ReactNode {
|
|
219
|
+
const tallPx = Number.parseInt(
|
|
220
|
+
new URLSearchParams(window.location.search).get("noteHeight") ?? "",
|
|
221
|
+
10,
|
|
222
|
+
);
|
|
223
|
+
return (
|
|
224
|
+
<div>
|
|
225
|
+
<p data-testid="order-internal-note">
|
|
226
|
+
Handled by Jonas Weber, escalate if unpaid past Sep 1.
|
|
227
|
+
</p>
|
|
228
|
+
{Number.isFinite(tallPx) && tallPx > 0 && (
|
|
229
|
+
<>
|
|
230
|
+
<div style={{ height: tallPx }} />
|
|
231
|
+
<p data-testid="order-internal-note-end">End of note</p>
|
|
232
|
+
</>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function Shell({
|
|
239
|
+
schema,
|
|
240
|
+
}: {
|
|
241
|
+
readonly children: ReactNode;
|
|
242
|
+
readonly schema: AppSchema;
|
|
243
|
+
}): ReactNode {
|
|
244
|
+
const feature = schema.features[0];
|
|
245
|
+
// KumikoScreen sits directly under DefaultAppShell's <main> — same as the
|
|
246
|
+
// real RoutedScreen (create-app.tsx) — so RenderEdit's `fillHeight` chain
|
|
247
|
+
// (h-full all the way down) resolves against `main`'s own, genuinely
|
|
248
|
+
// height-constrained box. A wrapper div here without its own h-full would
|
|
249
|
+
// break that percentage-height chain at the very first link.
|
|
250
|
+
return (
|
|
251
|
+
<DefaultAppShell
|
|
252
|
+
brand={<span>Order Desk</span>}
|
|
253
|
+
schema={schema}
|
|
254
|
+
user={{ id: "u1", roles: ["Admin"] }}
|
|
255
|
+
>
|
|
256
|
+
{feature !== undefined && (
|
|
257
|
+
<ExtensionSectionsProvider value={{ OrderInternalNote }}>
|
|
258
|
+
<KumikoScreen schema={feature} qn={ORDER_DETAIL_SCREEN_QN} entityId="order-1" />
|
|
259
|
+
</ExtensionSectionsProvider>
|
|
260
|
+
)}
|
|
261
|
+
</DefaultAppShell>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function boot(): Promise<void> {
|
|
266
|
+
const root = document.getElementById("root");
|
|
267
|
+
if (root === null) throw new Error("record-detail-layout/e2e: #root not found");
|
|
268
|
+
|
|
269
|
+
const response = await fetch("/schema.json");
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new Error(`record-detail-layout/e2e: /schema.json fetch failed with ${response.status}`);
|
|
272
|
+
}
|
|
273
|
+
const schema = (await response.json()) as AppSchema;
|
|
274
|
+
|
|
275
|
+
createKumikoApp({
|
|
276
|
+
schema,
|
|
277
|
+
dispatcher: createMockDispatcher(),
|
|
278
|
+
shell: Shell,
|
|
279
|
+
screenQn: ORDER_DETAIL_SCREEN_QN,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
boot().catch((error: unknown) => {
|
|
284
|
+
const root = document.getElementById("root");
|
|
285
|
+
if (root !== null) {
|
|
286
|
+
root.textContent = `record-detail-layout/e2e: boot failed — ${String(error)}`;
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### e2e/fixtures/mock-dispatcher.ts
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// In-memory dispatcher for record-detail-layout/e2e (fw#2778). Same shape as
|
|
296
|
+
// writeform-section/e2e/fixtures/mock-dispatcher.ts — implements only the
|
|
297
|
+
// three query QNs order-desk's projectionDetail screen needs:
|
|
298
|
+
//
|
|
299
|
+
// - "order-desk:query:order:detail" — the screen's own header/metrics row.
|
|
300
|
+
// - "order-desk:query:order:items" — the "items" relatedList tab.
|
|
301
|
+
// - "order-desk:query:order:payments" — the "payments" relatedList tab.
|
|
302
|
+
//
|
|
303
|
+
// The "items" row count is driven by the page's own `?items=<n>` URL param
|
|
304
|
+
// (read at query time, not baked into the fixture) so the same server can
|
|
305
|
+
// back both the short-list and long-list layout measurements the
|
|
306
|
+
// fw#2778 fix needs — a fixed row count would need two fixtures.
|
|
307
|
+
|
|
308
|
+
import type {
|
|
309
|
+
BatchResult,
|
|
310
|
+
Command,
|
|
311
|
+
Dispatcher,
|
|
312
|
+
DispatcherStatus,
|
|
313
|
+
PendingFile,
|
|
314
|
+
PendingWrite,
|
|
315
|
+
QueryOpts,
|
|
316
|
+
QueryResult,
|
|
317
|
+
Store,
|
|
318
|
+
WriteOpts,
|
|
319
|
+
WriteResult,
|
|
320
|
+
} from "@cosmicdrift/kumiko-renderer/headless";
|
|
321
|
+
|
|
322
|
+
const QUERY_ORDER_DETAIL = "order-desk:query:order:detail";
|
|
323
|
+
const QUERY_ORDER_ITEMS = "order-desk:query:order:items";
|
|
324
|
+
const QUERY_ORDER_PAYMENTS = "order-desk:query:order:payments";
|
|
325
|
+
|
|
326
|
+
const ORDER_DETAIL_FIELDS = {
|
|
327
|
+
customerName: "Aiko Tanaka",
|
|
328
|
+
orderNumber: "SO-10482",
|
|
329
|
+
status: "processing",
|
|
330
|
+
totalAmount: "€ 1,248.00",
|
|
331
|
+
outstandingAmount: "€ 312.00",
|
|
332
|
+
itemCount: "3",
|
|
333
|
+
placedAt: "2026-08-21",
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
function itemsCountFromUrl(): number {
|
|
337
|
+
const raw = new URLSearchParams(window.location.search).get("items");
|
|
338
|
+
const parsed = raw === null ? Number.NaN : Number.parseInt(raw, 10);
|
|
339
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildItemRows(count: number): Array<Record<string, unknown>> {
|
|
343
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
344
|
+
sku: `KB-${100 + i}`,
|
|
345
|
+
description: `Item ${i + 1}`,
|
|
346
|
+
quantity: 1,
|
|
347
|
+
unitPrice: 10 + i,
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const ONLINE_STORE: Store<DispatcherStatus> = {
|
|
352
|
+
getSnapshot: () => "online",
|
|
353
|
+
subscribe: () => () => {},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export function createMockDispatcher(): Dispatcher {
|
|
357
|
+
async function write<TData = unknown>(type: string): Promise<WriteResult<TData>> {
|
|
358
|
+
throw new Error(`mock-dispatcher: unsupported write qn "${type}"`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function query<TData = unknown>(
|
|
362
|
+
type: string,
|
|
363
|
+
payload: unknown,
|
|
364
|
+
_opts?: QueryOpts,
|
|
365
|
+
): Promise<QueryResult<TData>> {
|
|
366
|
+
if (type === QUERY_ORDER_DETAIL) {
|
|
367
|
+
const data = (payload ?? {}) as Record<string, unknown>; // @cast-boundary mock-dispatcher wire payload
|
|
368
|
+
const id = (data["id"] as string | undefined) ?? "order-1";
|
|
369
|
+
return { isSuccess: true, data: { id, ...ORDER_DETAIL_FIELDS } as unknown as TData };
|
|
370
|
+
}
|
|
371
|
+
if (type === QUERY_ORDER_ITEMS) {
|
|
372
|
+
return { isSuccess: true, data: { rows: buildItemRows(itemsCountFromUrl()) } as TData };
|
|
373
|
+
}
|
|
374
|
+
if (type === QUERY_ORDER_PAYMENTS) {
|
|
375
|
+
return {
|
|
376
|
+
isSuccess: true,
|
|
377
|
+
data: {
|
|
378
|
+
rows: [
|
|
379
|
+
{ paidAt: "2026-08-21", amount: 936, method: "card", status: "settled" },
|
|
380
|
+
{ paidAt: "2026-08-25", amount: 312, method: "invoice", status: "pending" },
|
|
381
|
+
],
|
|
382
|
+
} as TData,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
throw new Error(`mock-dispatcher: unsupported query qn "${type}"`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function batch(commands: readonly Command[], opts?: WriteOpts): Promise<BatchResult> {
|
|
389
|
+
const results: WriteResult[] = [];
|
|
390
|
+
for (const cmd of commands) results.push(await write(cmd.type, cmd.payload, opts));
|
|
391
|
+
return { isSuccess: true, results };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
write,
|
|
396
|
+
query,
|
|
397
|
+
batch,
|
|
398
|
+
statusStore: ONLINE_STORE,
|
|
399
|
+
async *stream() {},
|
|
400
|
+
pendingWrites: (): readonly PendingWrite[] => [],
|
|
401
|
+
pendingFiles: (): readonly PendingFile[] => [],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### e2e/height.spec.ts
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
// Tab-panel height spec (fw#2778): a relatedList tab in a projectionDetail
|
|
410
|
+
// "Akte" (order-detail's default "items" tab) must size to its content when
|
|
411
|
+
// the list is short, and cap at the panel's available height (scrolling
|
|
412
|
+
// internally) when the list is long — never stretch the card to the bottom
|
|
413
|
+
// of the panel regardless of row count. No hardcoded pixel positions: every
|
|
414
|
+
// assertion compares the card's live box model against `main`'s (the real,
|
|
415
|
+
// viewport-height-constrained ancestor from DefaultAppShell) and against
|
|
416
|
+
// itself across the two row-count scenarios.
|
|
417
|
+
//
|
|
418
|
+
// `?items=<n>` (read by fixtures/mock-dispatcher.ts) switches between the
|
|
419
|
+
// short-list and long-list scenario without needing two fixtures.
|
|
420
|
+
|
|
421
|
+
import { expect, boundaryGuardedTest as test } from "@cosmicdrift/kumiko-testing/e2e";
|
|
422
|
+
import type { Page } from "@playwright/test";
|
|
423
|
+
|
|
424
|
+
async function gotoOrderDetail(page: Page, items: number): Promise<void> {
|
|
425
|
+
await page.goto(`/?items=${items}`);
|
|
426
|
+
await expect(page.getByTestId("render-edit-form")).toBeVisible();
|
|
427
|
+
await expect(page.getByTestId("render-list-table")).toBeVisible();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function mainLocator(page: Page) {
|
|
431
|
+
return page.locator("main").first();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Structural, not class-name-coupled: FormScreenShell renders exactly one
|
|
435
|
+
// wrapping div holding [headerRegion-wrapper?, card] as direct children —
|
|
436
|
+
// the card (cardSurface()) is always the last of those (see DefaultForm in
|
|
437
|
+
// packages/renderer/src/web/primitives/index.tsx).
|
|
438
|
+
function cardLocator(page: Page) {
|
|
439
|
+
return page.locator('[data-testid="render-edit-form"] > div > div').last();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function tabsLocator(page: Page) {
|
|
443
|
+
return page.getByTestId("kumiko-screen-projection-detail-tabs");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
test.describe("record-detail-layout — tab-panel height (fw#2778)", () => {
|
|
447
|
+
// The design refresh (#3381) made detail tabs a full-bleed "board": the
|
|
448
|
+
// relatedList scroll region fills the panel down to the footer instead of
|
|
449
|
+
// the card sizing to its rows (fw#2778's original behavior). What must
|
|
450
|
+
// still hold: the rows themselves keep their natural height, and the
|
|
451
|
+
// region never exceeds the panel.
|
|
452
|
+
test("short list: the scroll region fills the panel while the table keeps its content height", async ({
|
|
453
|
+
page,
|
|
454
|
+
}) => {
|
|
455
|
+
await gotoOrderDetail(page, 3);
|
|
456
|
+
const main = await mainLocator(page).boundingBox();
|
|
457
|
+
const region = await page.getByTestId("render-list-table-scroll").boundingBox();
|
|
458
|
+
const table = await page.getByTestId("render-list-table").boundingBox();
|
|
459
|
+
if (main === null || region === null || table === null) throw new Error("missing bounding box");
|
|
460
|
+
expect(region.y + region.height).toBeLessThanOrEqual(main.y + main.height + 4);
|
|
461
|
+
expect(table.height).toBeLessThan(region.height);
|
|
462
|
+
expect(table.height).toBeLessThan(main.height * 0.4);
|
|
463
|
+
await expect(page.getByTestId("render-list-table-footer")).toBeVisible();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("long list: the tab-panel card is capped at the panel's available height and the table scrolls internally", async ({
|
|
467
|
+
page,
|
|
468
|
+
}) => {
|
|
469
|
+
await gotoOrderDetail(page, 60);
|
|
470
|
+
const main = await mainLocator(page).boundingBox();
|
|
471
|
+
const card = await cardLocator(page).boundingBox();
|
|
472
|
+
if (main === null || card === null) throw new Error("missing bounding box");
|
|
473
|
+
// Card never exceeds the panel height it's confined to (small tolerance
|
|
474
|
+
// for the header card's own margin above it).
|
|
475
|
+
expect(card.height).toBeLessThanOrEqual(main.height + 4);
|
|
476
|
+
|
|
477
|
+
// The document itself does not grow with the row count — the table
|
|
478
|
+
// scrolls INSIDE the card, the page never gets a body scrollbar.
|
|
479
|
+
const scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
480
|
+
const viewportHeight = await page.evaluate(() => window.innerHeight);
|
|
481
|
+
expect(scrollHeight).toBeLessThanOrEqual(viewportHeight + 4);
|
|
482
|
+
|
|
483
|
+
// Some ancestor between the <table> and the card actually clips and
|
|
484
|
+
// scrolls its content (tableInner's own `overflow-y-auto` div — the
|
|
485
|
+
// `<table>` sits inside an extra `overflow-x-auto` wrapper from
|
|
486
|
+
// ui/table.tsx first, whose reported `overflow-y` computed value is
|
|
487
|
+
// "auto" too per the CSS overflow-x/y coupling rule even though it
|
|
488
|
+
// never actually clips vertically, so this checks the real effect
|
|
489
|
+
// (scrollHeight > clientHeight) rather than the computed style). That's
|
|
490
|
+
// the "scrolls in place" half of the fix — the previous assertion
|
|
491
|
+
// already proved the card doesn't grow to make room for it instead.
|
|
492
|
+
const tableWrapperOverflows = await page.getByTestId("render-list-table").evaluate((table) => {
|
|
493
|
+
let el: HTMLElement | null = table.parentElement;
|
|
494
|
+
while (el !== null && el.scrollHeight <= el.clientHeight + 1) {
|
|
495
|
+
el = el.parentElement;
|
|
496
|
+
}
|
|
497
|
+
return el !== null;
|
|
498
|
+
});
|
|
499
|
+
expect(tableWrapperOverflows).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("record header (subtitle, metrics) and tab strip stay fully visible, uncompressed, in both scenarios", async ({
|
|
503
|
+
page,
|
|
504
|
+
}) => {
|
|
505
|
+
const subtitle = page.getByTestId("kumiko-screen-projection-detail-subtitle");
|
|
506
|
+
const metrics = page.getByTestId("kumiko-screen-projection-detail-metrics");
|
|
507
|
+
const heights: { tabs: number; subtitle: number; metrics: number }[] = [];
|
|
508
|
+
for (const items of [3, 60]) {
|
|
509
|
+
await gotoOrderDetail(page, items);
|
|
510
|
+
const tabs = await tabsLocator(page).boundingBox();
|
|
511
|
+
if (tabs === null) throw new Error(`missing tabs bounding box (items=${items})`);
|
|
512
|
+
expect(tabs.height).toBeGreaterThan(20);
|
|
513
|
+
// The title now lives in the shell's page header, not in the screen.
|
|
514
|
+
await expect(subtitle).toBeVisible();
|
|
515
|
+
await expect(metrics).toBeVisible();
|
|
516
|
+
const subtitleBox = await subtitle.boundingBox();
|
|
517
|
+
const metricsBox = await metrics.boundingBox();
|
|
518
|
+
if (subtitleBox === null || metricsBox === null) {
|
|
519
|
+
throw new Error(`missing header bounding box (items=${items})`);
|
|
520
|
+
}
|
|
521
|
+
heights.push({ tabs: tabs.height, subtitle: subtitleBox.height, metrics: metricsBox.height });
|
|
522
|
+
for (const region of [subtitle, metrics]) {
|
|
523
|
+
const clipped = await region.evaluate((el) => el.scrollHeight > el.clientHeight + 1);
|
|
524
|
+
expect(clipped).toBe(false);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const [few, many] = heights;
|
|
528
|
+
if (few === undefined || many === undefined) throw new Error("missing height samples");
|
|
529
|
+
expect(many.tabs).toBeCloseTo(few.tabs, 0);
|
|
530
|
+
expect(many.subtitle).toBeCloseTo(few.subtitle, 0);
|
|
531
|
+
expect(many.metrics).toBeCloseTo(few.metrics, 0);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test.describe("record-detail-layout — tall tab panel scrolls", () => {
|
|
536
|
+
test.use({ viewport: { width: 1440, height: 772 } });
|
|
537
|
+
|
|
538
|
+
test("an extension tab taller than the viewport scrolls its sections wrapper to the last element", async ({
|
|
539
|
+
page,
|
|
540
|
+
}) => {
|
|
541
|
+
await page.goto("/?tab=internal-note¬eHeight=1400");
|
|
542
|
+
const note = page.getByTestId("order-internal-note");
|
|
543
|
+
await expect(note).toBeVisible();
|
|
544
|
+
const end = page.getByTestId("order-internal-note-end");
|
|
545
|
+
|
|
546
|
+
// Nearest scrollable ancestor of the panel content: the sections wrapper.
|
|
547
|
+
const scroller = await note.evaluateHandle((el) => {
|
|
548
|
+
let node: HTMLElement | null = el.parentElement;
|
|
549
|
+
while (node !== null && !/(auto|scroll)/.test(getComputedStyle(node).overflowY)) {
|
|
550
|
+
node = node.parentElement;
|
|
551
|
+
}
|
|
552
|
+
if (node === null) throw new Error("no scrollable ancestor");
|
|
553
|
+
return node;
|
|
554
|
+
});
|
|
555
|
+
const metrics = () =>
|
|
556
|
+
scroller.evaluate((el) => ({
|
|
557
|
+
scrollHeight: el.scrollHeight,
|
|
558
|
+
clientHeight: el.clientHeight,
|
|
559
|
+
scrollTop: el.scrollTop,
|
|
560
|
+
}));
|
|
561
|
+
|
|
562
|
+
const before = await metrics();
|
|
563
|
+
expect(before.scrollHeight).toBeGreaterThan(before.clientHeight);
|
|
564
|
+
|
|
565
|
+
const box = await note.boundingBox();
|
|
566
|
+
if (box === null) throw new Error("missing note bounding box");
|
|
567
|
+
await page.mouse.move(box.x + 10, box.y + 10);
|
|
568
|
+
await page.mouse.wheel(0, 3000);
|
|
569
|
+
await expect.poll(async () => (await metrics()).scrollTop).toBeGreaterThan(0);
|
|
570
|
+
|
|
571
|
+
await expect(end).toBeInViewport();
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### e2e/padding-parity-screenshots.spec.ts
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
// Manual visual evidence for fw#3234 review round 2 (padding parity) — one
|
|
580
|
+
// screenshot per tab kind (fields, relatedList, extension) plus the head
|
|
581
|
+
// card + screen actions, Desktop and Phone (see playwright.config.ts
|
|
582
|
+
// "chromium-desktop-screenshots" / "chromium-phone" projects, enabled only
|
|
583
|
+
// with PADDING_PARITY_SCREENSHOTS=1). Not an
|
|
584
|
+
// assertion-bearing regression test (the DOM-level padding assertions live
|
|
585
|
+
// in projection-detail.test.tsx) — this only captures what a reviewer sees.
|
|
586
|
+
|
|
587
|
+
import { expect, boundaryGuardedTest as test } from "@cosmicdrift/kumiko-testing/e2e";
|
|
588
|
+
|
|
589
|
+
test("record-detail-layout — all tab kinds + screen actions, one screenshot per tab", async ({
|
|
590
|
+
page,
|
|
591
|
+
}, testInfo) => {
|
|
592
|
+
const tabIds = ["items", "payments", "details", "internal-note"];
|
|
593
|
+
for (const tabId of tabIds) {
|
|
594
|
+
await page.goto(`/?tab=${tabId}`);
|
|
595
|
+
await expect(page.getByTestId("render-edit-form")).toBeVisible();
|
|
596
|
+
await expect(page.getByTestId("kumiko-screen-projection-detail-tabs")).toBeVisible();
|
|
597
|
+
if (tabId === "internal-note") {
|
|
598
|
+
await expect(page.getByTestId("order-internal-note")).toBeVisible();
|
|
599
|
+
}
|
|
600
|
+
await page.screenshot({
|
|
601
|
+
path: testInfo.outputPath(`${tabId}.png`),
|
|
602
|
+
fullPage: true,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### e2e/phone-viewport.spec.ts
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
// Phone-viewport tab strip (fw#3234): order-detail's tabs Card frame must
|
|
612
|
+
// stay within the panel's own width on a narrow phone viewport — no
|
|
613
|
+
// horizontal document scroll from the tab strip or the Card chrome around
|
|
614
|
+
// it. Runs against the "chromium-phone" project (see playwright.config.ts),
|
|
615
|
+
// a real iPhone-sized viewport, not a resized desktop window.
|
|
616
|
+
|
|
617
|
+
import { expect, boundaryGuardedTest as test } from "@cosmicdrift/kumiko-testing/e2e";
|
|
618
|
+
|
|
619
|
+
test("record-detail-layout — tab strip fits the phone viewport width (fw#3234)", async ({
|
|
620
|
+
page,
|
|
621
|
+
}) => {
|
|
622
|
+
await page.goto("/");
|
|
623
|
+
await expect(page.getByTestId("render-edit-form")).toBeVisible();
|
|
624
|
+
await expect(page.getByTestId("kumiko-screen-projection-detail-tabs")).toBeVisible();
|
|
625
|
+
|
|
626
|
+
const overflowsHorizontally = await page.evaluate(
|
|
627
|
+
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
|
628
|
+
);
|
|
629
|
+
expect(overflowsHorizontally).toBe(false);
|
|
630
|
+
|
|
631
|
+
const tabsBox = await page
|
|
632
|
+
.getByTestId("kumiko-screen-projection-detail-tabs")
|
|
633
|
+
.evaluate((el) => ({ scrollWidth: el.scrollWidth, clientWidth: el.clientWidth }));
|
|
634
|
+
expect(tabsBox.scrollWidth).toBeLessThanOrEqual(tabsBox.clientWidth);
|
|
635
|
+
});
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### e2e/playwright.config.ts
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
// Playwright config for record-detail-layout/e2e. Boots the minimal
|
|
642
|
+
// build-server (see e2e/build-server.ts) — a real browser + real React
|
|
643
|
+
// bundle against a MockDispatcher, no full Kumiko stack. Pattern copied
|
|
644
|
+
// from samples/recipes/writeform-section/e2e/playwright.config.ts.
|
|
645
|
+
//
|
|
646
|
+
// Port 4190: see the build-server.ts comment for the port allocation
|
|
647
|
+
// overview of the other samples/apps ports.
|
|
648
|
+
//
|
|
649
|
+
// Config lives IN e2e/ itself — Playwright's webServer.command runs by
|
|
650
|
+
// default with cwd = the config's directory, i.e. relative to e2e/ itself
|
|
651
|
+
// (not to the package root). testDir therefore stays "." instead of
|
|
652
|
+
// "./e2e", and webServer.command calls build-server.ts without an "e2e/"
|
|
653
|
+
// prefix.
|
|
654
|
+
|
|
655
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
656
|
+
|
|
657
|
+
// The padding-parity spec only captures screenshots for manual review (no
|
|
658
|
+
// regression assertions), so it stays out of regular runs and costs no runner
|
|
659
|
+
// minutes unless this flag is set.
|
|
660
|
+
const WITH_PARITY_SCREENSHOTS = !!process.env["PADDING_PARITY_SCREENSHOTS"];
|
|
661
|
+
|
|
662
|
+
const PORT = 4190;
|
|
663
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
664
|
+
|
|
665
|
+
export default defineConfig({
|
|
666
|
+
testDir: ".",
|
|
667
|
+
testIgnore: ["**/fixtures/**", "build-server.ts"],
|
|
668
|
+
fullyParallel: false,
|
|
669
|
+
forbidOnly: !!process.env["CI"],
|
|
670
|
+
retries: 0,
|
|
671
|
+
workers: 1,
|
|
672
|
+
reporter: [["list"]],
|
|
673
|
+
timeout: 15_000,
|
|
674
|
+
expect: { timeout: 3_000 },
|
|
675
|
+
|
|
676
|
+
use: {
|
|
677
|
+
baseURL: BASE_URL,
|
|
678
|
+
trace: "retain-on-failure",
|
|
679
|
+
screenshot: "only-on-failure",
|
|
680
|
+
actionTimeout: 5_000,
|
|
681
|
+
navigationTimeout: 10_000,
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
projects: [
|
|
685
|
+
{
|
|
686
|
+
name: "chromium",
|
|
687
|
+
testIgnore: /(phone-viewport|padding-parity-screenshots)\.spec\.ts$/,
|
|
688
|
+
use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } },
|
|
689
|
+
},
|
|
690
|
+
// Desktop copy of padding-parity-screenshots.spec.ts, distinct from the
|
|
691
|
+
// "chromium" project above (that one ignores it) so the screenshot spec
|
|
692
|
+
// gets its own explicit project name instead of hiding in "chromium"'s
|
|
693
|
+
// catch-all — matches the phone project's split for the same reason.
|
|
694
|
+
...(WITH_PARITY_SCREENSHOTS
|
|
695
|
+
? [
|
|
696
|
+
{
|
|
697
|
+
name: "chromium-desktop-screenshots",
|
|
698
|
+
testMatch: /padding-parity-screenshots\.spec\.ts$/,
|
|
699
|
+
use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } },
|
|
700
|
+
},
|
|
701
|
+
]
|
|
702
|
+
: []),
|
|
703
|
+
// Phone viewport (fw#3234): the tab strip must stay within the panel
|
|
704
|
+
// width — no horizontal document scroll — down to a narrow phone frame.
|
|
705
|
+
// Plain Desktop Chrome with an iPhone-sized viewport, not the "iPhone
|
|
706
|
+
// 13" device preset (isMobile/hasTouch touch emulation is flaky in this
|
|
707
|
+
// sandbox's headless chromium) — width is what fw#3234 cares about.
|
|
708
|
+
{
|
|
709
|
+
name: "chromium-phone",
|
|
710
|
+
testMatch: WITH_PARITY_SCREENSHOTS
|
|
711
|
+
? /(phone-viewport|padding-parity-screenshots)\.spec\.ts$/
|
|
712
|
+
: /phone-viewport\.spec\.ts$/,
|
|
713
|
+
use: { ...devices["Desktop Chrome"], viewport: { width: 390, height: 844 } },
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
|
|
717
|
+
webServer: {
|
|
718
|
+
command: "bun build-server.ts",
|
|
719
|
+
url: BASE_URL,
|
|
720
|
+
env: { PORT: String(PORT) },
|
|
721
|
+
reuseExistingServer: !process.env["CI"],
|
|
722
|
+
timeout: 30_000,
|
|
723
|
+
stdout: "pipe",
|
|
724
|
+
stderr: "pipe",
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
```
|
|
728
|
+
|
|
37
729
|
### src/__tests__/feature.test.ts
|
|
38
730
|
|
|
39
731
|
```ts
|