@nexttylabs/echo 0.3.0 → 0.5.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/CHANGELOG.md +25 -0
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
- package/app/api/admin/backup/route.ts +22 -4
- package/app/api/auth/register/handler.ts +1 -2
- package/lib/auth/config.ts +0 -7
- package/lib/db/migrations/0000_needy_leech.sql +335 -0
- package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
- package/lib/db/migrations/meta/_journal.json +2 -135
- package/lib/db/schema/auth.ts +0 -1
- package/lib/db/schema/index.ts +0 -1
- package/lib/portal/public-context.tsx +5 -0
- package/package.json +20 -1
- package/.changeset/README.md +0 -21
- package/.changeset/config.json +0 -11
- package/.changeset/cozy-ghosts-care.md +0 -5
- package/.changeset/sharp-lines-stand.md +0 -5
- package/.changeset/sour-doodles-eat.md +0 -5
- package/.changeset/tender-moose-shop.md +0 -5
- package/.github/pull_request_template.md +0 -13
- package/.github/workflows/ci.yml +0 -41
- package/.github/workflows/publish.yml +0 -44
- package/.github/workflows/release.yml +0 -73
- package/AGENTS.md +0 -92
- package/Dockerfile +0 -57
- package/Makefile +0 -77
- package/app/api/internal/domain-lookup/route.ts +0 -67
- package/bun.lock +0 -2503
- package/components/portal/project-switcher.tsx +0 -20
- package/docker-compose.dev.yml +0 -26
- package/docker-compose.yml +0 -98
- package/docs/architecture.md +0 -259
- package/docs/component-inventory.md +0 -261
- package/docs/database-migrations.md +0 -76
- package/docs/development-guide.md +0 -209
- package/docs/e2e-user-flows.csv +0 -31
- package/docs/er-diagram-feedback.mmd +0 -138
- package/docs/er-diagram.mmd +0 -281
- package/docs/i18n-check-report.md +0 -296
- package/docs/index.md +0 -214
- package/docs/logic-chain.md +0 -94
- package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
- package/docs/plans/2026-01-02-user-login-design.md +0 -37
- package/docs/plans/2026-01-02-user-login.md +0 -437
- package/docs/plans/2026-01-02-user-registration-design.md +0 -47
- package/docs/plans/2026-01-02-user-registration.md +0 -628
- package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
- package/docs/plans/2026-01-03-roles-permissions.md +0 -266
- package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
- package/docs/plans/2026-01-05-member-removal.md +0 -186
- package/docs/plans/2026-01-05-organization-creation.md +0 -374
- package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
- package/docs/plans/2026-01-05-role-configuration.md +0 -441
- package/docs/plans/2026-01-06-file-upload-support.md +0 -804
- package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
- package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
- package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
- package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
- package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
- package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
- package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
- package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
- package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
- package/docs/plans/2026-01-09-settings-center-design.md +0 -114
- package/docs/plans/2026-01-09-settings-center.md +0 -948
- package/docs/plans/2026-01-10-organization-only-design.md +0 -66
- package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
- package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
- package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
- package/docs/plans/2026-01-18-changesets-design.md +0 -40
- package/docs/product_changes.md +0 -37
- package/docs/project-overview.md +0 -159
- package/docs/project-scan-report.json +0 -104
- package/docs/route-role-visibility.md +0 -51
- package/docs/source-tree-analysis.md +0 -150
- package/docs/testing/delete-project-manual-tests.md +0 -18
- package/docs/user-story-tracking.md +0 -191
- package/eslint.config.mjs +0 -19
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
- package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
- package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
- package/lib/db/migrations/0003_add_org_description.sql +0 -1
- package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
- package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
- package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
- package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
- package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
- package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
- package/lib/db/migrations/0010_kind_junta.sql +0 -8
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
- package/lib/db/migrations/0012_giant_power_man.sql +0 -24
- package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
- package/lib/db/migrations/0014_blue_alice.sql +0 -18
- package/lib/db/migrations/0015_webhook_tables.sql +0 -41
- package/lib/db/migrations/0016_github_integration.sql +0 -30
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
- package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
- package/lib/db/migrations/0018_same_spitfire.sql +0 -1
- package/lib/db/migrations/0019_jittery_loners.sql +0 -16
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
- package/lib/db/migrations/meta/0001_snapshot.json +0 -553
- package/lib/db/migrations/meta/0002_snapshot.json +0 -560
- package/lib/db/migrations/meta/0003_snapshot.json +0 -650
- package/lib/db/migrations/meta/0004_snapshot.json +0 -852
- package/lib/db/migrations/meta/0005_snapshot.json +0 -900
- package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
- package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
- package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
- package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
- package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
- package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
- package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
- package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
- package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
- package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
- package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
- package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
- package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
- package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
- package/lib/db/schema/projects.ts +0 -145
- package/lib/db/schema/user-profiles.ts +0 -31
- package/lib/validations/projects.ts +0 -49
- package/next-env.d.ts +0 -6
- package/playwright.config.ts +0 -44
- package/proxy.test.ts +0 -131
- package/proxy.ts +0 -190
- package/scripts/backup-db.sh +0 -57
- package/scripts/backup-db.ts +0 -24
- package/scripts/generate-openapi.ts +0 -22
- package/scripts/migration-helper.ts +0 -39
- package/scripts/pre-deploy.ts +0 -75
- package/scripts/restore-db.sh +0 -60
- package/scripts/rollback.ts +0 -72
- package/scripts/seed-tags.ts +0 -48
- package/tests/api/feedback-bulk.test.ts +0 -47
- package/tests/api/feedback-by-id.test.ts +0 -67
- package/tests/api/feedback-comments-route-import.test.ts +0 -26
- package/tests/api/feedback-create.test.ts +0 -71
- package/tests/api/feedback-delete.test.ts +0 -160
- package/tests/api/feedback-filter.test.ts +0 -250
- package/tests/api/feedback-list.test.ts +0 -234
- package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
- package/tests/api/feedback-similar.test.ts +0 -46
- package/tests/api/feedback-sort.test.ts +0 -261
- package/tests/api/feedback-status-enum.test.ts +0 -49
- package/tests/api/feedback-status-filter.test.ts +0 -117
- package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
- package/tests/api/feedback.test.ts +0 -175
- package/tests/api/identify-jwt.test.ts +0 -25
- package/tests/api/invitation-accept.test.ts +0 -213
- package/tests/api/organization-invitations.test.ts +0 -186
- package/tests/api/organization-members-list.test.ts +0 -79
- package/tests/api/organization-members.test.ts +0 -340
- package/tests/api/organizations.test.ts +0 -149
- package/tests/api/register.test.ts +0 -112
- package/tests/api/upload.test.ts +0 -103
- package/tests/api/vote.test.ts +0 -82
- package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
- package/tests/app/admin-feedback-list-page.test.tsx +0 -25
- package/tests/app/admin-feedback-new-page.test.tsx +0 -25
- package/tests/app/health-route-helpers.test.ts +0 -27
- package/tests/app/login-page.test.ts +0 -26
- package/tests/app/portal-page.test.ts +0 -29
- package/tests/app/project-portal-overview.test.tsx +0 -25
- package/tests/app/widget-page-import.test.ts +0 -25
- package/tests/components/create-post-dialog-defaults.test.ts +0 -43
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
- package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
- package/tests/components/feedback/feedback-detail.test.tsx +0 -25
- package/tests/components/feedback/feedback-stats.test.tsx +0 -49
- package/tests/components/feedback-bulk-actions.test.tsx +0 -39
- package/tests/components/feedback-i18n-keys.test.ts +0 -70
- package/tests/components/feedback-list-controls-compile.test.ts +0 -25
- package/tests/components/feedback-list-controls.test.tsx +0 -204
- package/tests/components/feedback-list-item.test.tsx +0 -67
- package/tests/components/landing/hero.test.tsx +0 -46
- package/tests/components/layout/language-switcher.test.tsx +0 -25
- package/tests/components/layout/sidebar.test.tsx +0 -157
- package/tests/components/login-form.test.ts +0 -25
- package/tests/components/organization-form.test.ts +0 -32
- package/tests/components/organization-switcher.test.ts +0 -25
- package/tests/components/pagination.test.tsx +0 -43
- package/tests/components/portal-overview.test.tsx +0 -25
- package/tests/components/profile-form.test.tsx +0 -139
- package/tests/components/role-selector.test.ts +0 -31
- package/tests/components/status-chart.test.tsx +0 -90
- package/tests/e2e/auth.e2e.ts +0 -323
- package/tests/e2e/feedback-actions.e2e.ts +0 -471
- package/tests/e2e/feedback-attachment.e2e.ts +0 -168
- package/tests/e2e/feedback-customer.e2e.ts +0 -226
- package/tests/e2e/feedback-management.e2e.ts +0 -565
- package/tests/e2e/feedback-submit.e2e.ts +0 -133
- package/tests/e2e/feedback-view.e2e.ts +0 -297
- package/tests/e2e/fixtures/test-data.ts +0 -235
- package/tests/e2e/health-check.e2e.ts +0 -230
- package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
- package/tests/e2e/helpers/test-utils.ts +0 -298
- package/tests/e2e/integration-placeholders.e2e.ts +0 -199
- package/tests/e2e/organization.e2e.ts +0 -292
- package/tests/e2e/permissions.e2e.ts +0 -424
- package/tests/e2e/project-widget.e2e.ts +0 -63
- package/tests/feedback/filters.test.ts +0 -29
- package/tests/hooks/use-permissions.test.ts +0 -52
- package/tests/lib/ai/classifier.test.ts +0 -104
- package/tests/lib/ai/duplicate-detector.test.ts +0 -234
- package/tests/lib/attachments-schema.test.ts +0 -30
- package/tests/lib/auth/session.test.ts +0 -49
- package/tests/lib/auth-client.test.ts +0 -37
- package/tests/lib/auth-config.test.ts +0 -26
- package/tests/lib/feedback-prefill.test.ts +0 -52
- package/tests/lib/feedback-processor.test.ts +0 -41
- package/tests/lib/feedback-schema.test.ts +0 -33
- package/tests/lib/file-validator.test.ts +0 -48
- package/tests/lib/get-feedback-by-id.test.ts +0 -37
- package/tests/lib/invitations.test.ts +0 -35
- package/tests/lib/login-schema.test.ts +0 -36
- package/tests/lib/org-context.test.ts +0 -95
- package/tests/lib/organization-access.test.ts +0 -44
- package/tests/lib/organization-member-role-schema.test.ts +0 -41
- package/tests/lib/permissions.test.ts +0 -88
- package/tests/lib/portal-analytics.test.ts +0 -25
- package/tests/lib/portal-contributors.test.ts +0 -25
- package/tests/lib/portal-copy.test.ts +0 -27
- package/tests/lib/portal-i18n.test.ts +0 -30
- package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
- package/tests/lib/portal-modules.test.ts +0 -25
- package/tests/lib/portal-seo.test.ts +0 -25
- package/tests/lib/portal-sharing.test.ts +0 -25
- package/tests/lib/portal-sorting.test.ts +0 -25
- package/tests/lib/portal-theme.test.ts +0 -25
- package/tests/lib/rate-limit.test.ts +0 -142
- package/tests/lib/resolve-locale.test.ts +0 -34
- package/tests/lib/services/backup.test.ts +0 -145
- package/tests/lib/user-organizations.test.ts +0 -42
- package/tests/lib/user-role-schema.test.ts +0 -33
- package/tests/lib/user-schema.test.ts +0 -25
- package/tests/setup.ts +0 -74
- package/vercel.json +0 -4
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, expect, it, mock } from "bun:test";
|
|
19
|
-
import { render } from "@testing-library/react";
|
|
20
|
-
import { ProfileForm } from "@/components/settings/profile-form";
|
|
21
|
-
import "../setup";
|
|
22
|
-
|
|
23
|
-
// Mock translations
|
|
24
|
-
const translations: Record<string, string> = {
|
|
25
|
-
cardTitle: "Personal Information",
|
|
26
|
-
cardDescription: "Update your personal details",
|
|
27
|
-
nameLabel: "Name",
|
|
28
|
-
namePlaceholder: "Enter your name",
|
|
29
|
-
nameRequired: "Name is required",
|
|
30
|
-
nameTooLong: "Name must be 100 characters or less",
|
|
31
|
-
emailLabel: "Email",
|
|
32
|
-
emailNote: "Email address cannot be changed",
|
|
33
|
-
saveChanges: "Save Changes",
|
|
34
|
-
updating: "Saving...",
|
|
35
|
-
updateSuccess: "Profile updated successfully",
|
|
36
|
-
updateFailed: "Failed to update profile",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
mock.module("next-intl", () => ({
|
|
40
|
-
useTranslations: () => (key: string) => translations[key] ?? key,
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
// Mock authClient
|
|
44
|
-
mock.module("@/lib/auth/client", () => ({
|
|
45
|
-
authClient: {
|
|
46
|
-
useSession: () => ({
|
|
47
|
-
data: {
|
|
48
|
-
user: {
|
|
49
|
-
id: "test-user-id",
|
|
50
|
-
name: "Test User",
|
|
51
|
-
email: "test@example.com",
|
|
52
|
-
image: null,
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
refetch: () => Promise.resolve(),
|
|
56
|
-
}),
|
|
57
|
-
updateUser: () => Promise.resolve(),
|
|
58
|
-
},
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
// Mock UI components
|
|
62
|
-
mock.module("@/components/ui/card", () => ({
|
|
63
|
-
Card: ({ children }: { children: React.ReactNode }) => <div data-testid="card">{children}</div>,
|
|
64
|
-
CardHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
65
|
-
CardTitle: ({ children }: { children: React.ReactNode }) => <h3>{children}</h3>,
|
|
66
|
-
CardDescription: ({ children }: { children: React.ReactNode }) => <p>{children}</p>,
|
|
67
|
-
CardContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
mock.module("@/components/ui/label", () => ({
|
|
71
|
-
Label: ({ children, htmlFor }: { children: React.ReactNode; htmlFor?: string }) => (
|
|
72
|
-
<label htmlFor={htmlFor}>{children}</label>
|
|
73
|
-
),
|
|
74
|
-
}));
|
|
75
|
-
|
|
76
|
-
mock.module("@/components/ui/input", () => ({
|
|
77
|
-
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
|
-
mock.module("@/components/ui/button", () => ({
|
|
81
|
-
Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { children: React.ReactNode }) => (
|
|
82
|
-
<button {...props}>{children}</button>
|
|
83
|
-
),
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
describe("ProfileForm", () => {
|
|
87
|
-
it("renders the profile form with user data", () => {
|
|
88
|
-
const { getByText, getAllByDisplayValue } = render(<ProfileForm />);
|
|
89
|
-
|
|
90
|
-
// Check that form titles are rendered
|
|
91
|
-
expect(getByText("Personal Information")).toBeDefined();
|
|
92
|
-
expect(getByText("Update your personal details")).toBeDefined();
|
|
93
|
-
|
|
94
|
-
// Check that user data is displayed
|
|
95
|
-
const nameInputs = getAllByDisplayValue("Test User");
|
|
96
|
-
const emailInputs = getAllByDisplayValue("test@example.com");
|
|
97
|
-
expect(nameInputs.length).toBeGreaterThan(0);
|
|
98
|
-
expect(emailInputs.length).toBeGreaterThan(0);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("renders the save button", () => {
|
|
102
|
-
const { getAllByText } = render(<ProfileForm />);
|
|
103
|
-
const saveButtons = getAllByText("Save Changes");
|
|
104
|
-
expect(saveButtons.length).toBeGreaterThan(0);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("disables save button initially when form is not dirty", () => {
|
|
108
|
-
const { getAllByText } = render(<ProfileForm />);
|
|
109
|
-
const saveButtons = getAllByText("Save Changes");
|
|
110
|
-
const saveButton = saveButtons[0].closest("button");
|
|
111
|
-
// Button is disabled initially because isDirty is false
|
|
112
|
-
expect(saveButton?.disabled).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("disables email input field", () => {
|
|
116
|
-
const { getAllByDisplayValue } = render(<ProfileForm />);
|
|
117
|
-
const emailInputs = getAllByDisplayValue("test@example.com");
|
|
118
|
-
const emailInput = emailInputs[0];
|
|
119
|
-
expect(emailInput.hasAttribute("disabled")).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("shows email note explaining email cannot be changed", () => {
|
|
123
|
-
const { getAllByText } = render(<ProfileForm />);
|
|
124
|
-
const notes = getAllByText("Email address cannot be changed");
|
|
125
|
-
expect(notes.length).toBeGreaterThan(0);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("renders name input with placeholder", () => {
|
|
129
|
-
const { getAllByPlaceholderText } = render(<ProfileForm />);
|
|
130
|
-
const inputs = getAllByPlaceholderText("Enter your name");
|
|
131
|
-
expect(inputs.length).toBeGreaterThan(0);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("renders within a card component", () => {
|
|
135
|
-
const { getAllByTestId } = render(<ProfileForm />);
|
|
136
|
-
const cards = getAllByTestId("card");
|
|
137
|
-
expect(cards.length).toBeGreaterThan(0);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, expect, it } from "bun:test";
|
|
19
|
-
import { ROLE_OPTIONS } from "@/components/settings/role-selector";
|
|
20
|
-
|
|
21
|
-
describe("ROLE_OPTIONS", () => {
|
|
22
|
-
it("includes all organization roles", () => {
|
|
23
|
-
const roles = ROLE_OPTIONS.map((option) => option.value);
|
|
24
|
-
expect(roles).toEqual([
|
|
25
|
-
"admin",
|
|
26
|
-
"product_manager",
|
|
27
|
-
"developer",
|
|
28
|
-
"customer_support",
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import React from "react";
|
|
19
|
-
import { afterEach, beforeAll, describe, expect, it, mock } from "bun:test";
|
|
20
|
-
import { cleanup, render } from "@testing-library/react";
|
|
21
|
-
import "../setup";
|
|
22
|
-
|
|
23
|
-
let StatusChart: typeof import("@/components/dashboard/status-chart").StatusChart;
|
|
24
|
-
|
|
25
|
-
mock.module("next-intl", () => ({
|
|
26
|
-
useTranslations: (namespace: string) => (key: string) => {
|
|
27
|
-
if (namespace === "dashboard.statusChart") {
|
|
28
|
-
if (key === "title") {
|
|
29
|
-
return "Status Distribution";
|
|
30
|
-
}
|
|
31
|
-
if (key === "noData") {
|
|
32
|
-
return "No data";
|
|
33
|
-
}
|
|
34
|
-
return key;
|
|
35
|
-
}
|
|
36
|
-
if (namespace === "feedback") {
|
|
37
|
-
const messages: Record<string, string> = {
|
|
38
|
-
"status.inProgress": "In Progress",
|
|
39
|
-
"filters.title": "Status",
|
|
40
|
-
};
|
|
41
|
-
if (key in messages) {
|
|
42
|
-
return messages[key];
|
|
43
|
-
}
|
|
44
|
-
throw new Error(
|
|
45
|
-
`MISSING_MESSAGE: Could not resolve ${namespace}.${key} in messages for locale en`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return key;
|
|
49
|
-
},
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
mock.module("recharts", () => ({
|
|
53
|
-
ResponsiveContainer: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
54
|
-
PieChart: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
55
|
-
Pie: ({
|
|
56
|
-
children,
|
|
57
|
-
data,
|
|
58
|
-
}: {
|
|
59
|
-
children: React.ReactNode;
|
|
60
|
-
data?: { name: string }[];
|
|
61
|
-
}) => (
|
|
62
|
-
<div>
|
|
63
|
-
{data?.map((entry) => (
|
|
64
|
-
<span key={entry.name}>{entry.name}</span>
|
|
65
|
-
))}
|
|
66
|
-
{children}
|
|
67
|
-
</div>
|
|
68
|
-
),
|
|
69
|
-
Cell: () => <div />,
|
|
70
|
-
Legend: () => <div />,
|
|
71
|
-
Tooltip: () => <div />,
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
beforeAll(async () => {
|
|
75
|
-
({ StatusChart } = await import("@/components/dashboard/status-chart"));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
afterEach(() => {
|
|
79
|
-
cleanup();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("StatusChart", () => {
|
|
83
|
-
it("renders in-progress label without missing message errors", () => {
|
|
84
|
-
const { getByText } = render(
|
|
85
|
-
<StatusChart data={[{ status: "in-progress", count: 2 }]} />,
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
expect(getByText("In Progress")).toBeTruthy();
|
|
89
|
-
});
|
|
90
|
-
});
|
package/tests/e2e/auth.e2e.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2026 Echo Team
|
|
3
|
-
*
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
-
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
* (at your option) any later version.
|
|
8
|
-
*
|
|
9
|
-
* This program is distributed in the hope that it will be useful,
|
|
10
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
* GNU Affero General Public License for more details.
|
|
13
|
-
*
|
|
14
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { test, expect, type Page } from "@playwright/test";
|
|
19
|
-
import { uniqueEmail, uniqueName, TestHelpers } from "./helpers/test-utils";
|
|
20
|
-
|
|
21
|
-
const userMenuButton = (page: Page, name: string) =>
|
|
22
|
-
page.getByRole("button", { name: new RegExp(name, "i") }).first();
|
|
23
|
-
|
|
24
|
-
test.describe("E2E-UF-006: Team member login", () => {
|
|
25
|
-
const password = "StrongPass123!";
|
|
26
|
-
|
|
27
|
-
test("logs in with valid credentials", async ({ page, request }) => {
|
|
28
|
-
const helpers = new TestHelpers(page, request);
|
|
29
|
-
const email = uniqueEmail();
|
|
30
|
-
const name = uniqueName();
|
|
31
|
-
|
|
32
|
-
// First register a user
|
|
33
|
-
await helpers.registerAndLogin(name, email, password);
|
|
34
|
-
|
|
35
|
-
// Logout (clear session cookies)
|
|
36
|
-
await page.request.post("/api/auth/clear-session");
|
|
37
|
-
|
|
38
|
-
// Now test login
|
|
39
|
-
await page.goto("/login");
|
|
40
|
-
|
|
41
|
-
// Fill login form
|
|
42
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
43
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
44
|
-
|
|
45
|
-
// Submit login
|
|
46
|
-
await page
|
|
47
|
-
.getByRole("button", { name: /sign in|log in|login|登录|ログイン/i })
|
|
48
|
-
.click();
|
|
49
|
-
|
|
50
|
-
// Should redirect to dashboard
|
|
51
|
-
await page.waitForURL(/\/dashboard/);
|
|
52
|
-
|
|
53
|
-
// Verify successful login
|
|
54
|
-
await expect(userMenuButton(page, name)).toBeVisible();
|
|
55
|
-
|
|
56
|
-
// Check if we can access protected pages
|
|
57
|
-
await page.goto("/dashboard");
|
|
58
|
-
await expect(page.url()).toContain("/dashboard");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("shows error for invalid credentials", async ({ page }) => {
|
|
62
|
-
await page.goto("/login");
|
|
63
|
-
|
|
64
|
-
// Try to login with invalid credentials
|
|
65
|
-
await page.locator('input[name="email"], #email').fill("invalid@example.com");
|
|
66
|
-
await page.locator('input[name="password"], #password').fill("wrongpassword");
|
|
67
|
-
|
|
68
|
-
await page
|
|
69
|
-
.getByRole("button", { name: /sign in|log in|login|登录|ログイン/i })
|
|
70
|
-
.click();
|
|
71
|
-
|
|
72
|
-
// Should show error message
|
|
73
|
-
await expect(page.getByText(/invalid|incorrect|failed|邮箱|メールアドレス|密码|パスワード/i)).toBeVisible();
|
|
74
|
-
|
|
75
|
-
// Should stay on login page
|
|
76
|
-
expect(page.url()).toContain("/login");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("remembers user session after page refresh", async ({ page, request }) => {
|
|
80
|
-
const helpers = new TestHelpers(page, request);
|
|
81
|
-
const email = uniqueEmail();
|
|
82
|
-
const name = uniqueName();
|
|
83
|
-
|
|
84
|
-
// Register and login
|
|
85
|
-
await helpers.registerAndLogin(name, email, password);
|
|
86
|
-
|
|
87
|
-
// Refresh page
|
|
88
|
-
await page.reload();
|
|
89
|
-
|
|
90
|
-
// Should still be logged in
|
|
91
|
-
await expect(userMenuButton(page, name)).toBeVisible();
|
|
92
|
-
|
|
93
|
-
// Should still be able to access protected pages
|
|
94
|
-
await page.goto("/dashboard");
|
|
95
|
-
await expect(page.url()).toContain("/dashboard");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("redirects to intended page after login", async ({ page, request }) => {
|
|
99
|
-
const helpers = new TestHelpers(page, request);
|
|
100
|
-
const email = uniqueEmail();
|
|
101
|
-
const name = uniqueName();
|
|
102
|
-
|
|
103
|
-
// Register user
|
|
104
|
-
await helpers.registerAndLogin(name, email, password);
|
|
105
|
-
await page.request.post("/api/auth/clear-session");
|
|
106
|
-
|
|
107
|
-
// Try to access a protected page directly
|
|
108
|
-
const protectedPage = "/dashboard";
|
|
109
|
-
await page.goto(protectedPage);
|
|
110
|
-
|
|
111
|
-
// Should redirect to login
|
|
112
|
-
await page.waitForURL(/\/login/);
|
|
113
|
-
|
|
114
|
-
// Login
|
|
115
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
116
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
117
|
-
await page
|
|
118
|
-
.getByRole("button", { name: /sign in|log in|login|登录|ログイン/i })
|
|
119
|
-
.click();
|
|
120
|
-
|
|
121
|
-
// Login flow always redirects to dashboard
|
|
122
|
-
await page.waitForURL(/\/dashboard/);
|
|
123
|
-
await expect(page.url()).toContain("/dashboard");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("logs out successfully", async ({ page, request }) => {
|
|
127
|
-
const helpers = new TestHelpers(page, request);
|
|
128
|
-
const email = uniqueEmail();
|
|
129
|
-
const name = uniqueName();
|
|
130
|
-
|
|
131
|
-
// Register and login
|
|
132
|
-
await helpers.registerAndLogin(name, email, password);
|
|
133
|
-
|
|
134
|
-
// Logout
|
|
135
|
-
await page.request.post("/api/auth/clear-session");
|
|
136
|
-
|
|
137
|
-
// Try to access protected page
|
|
138
|
-
await page.goto("/dashboard");
|
|
139
|
-
|
|
140
|
-
// Should redirect to login
|
|
141
|
-
await page.waitForURL(/\/login/);
|
|
142
|
-
|
|
143
|
-
// Verify no user info is visible
|
|
144
|
-
await expect(userMenuButton(page, name)).not.toBeVisible();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test.describe("E2E-UF-020: User registration", () => {
|
|
149
|
-
const password = "StrongPass123!";
|
|
150
|
-
|
|
151
|
-
test("registers new user successfully", async ({ page }) => {
|
|
152
|
-
await page.goto("/register");
|
|
153
|
-
|
|
154
|
-
const email = uniqueEmail();
|
|
155
|
-
const name = uniqueName();
|
|
156
|
-
|
|
157
|
-
// Fill registration form
|
|
158
|
-
await page.locator('input[name="name"], #name').fill(name);
|
|
159
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
160
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
161
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill(password);
|
|
162
|
-
|
|
163
|
-
// Submit registration
|
|
164
|
-
await page
|
|
165
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
166
|
-
.click();
|
|
167
|
-
|
|
168
|
-
// Should redirect to dashboard
|
|
169
|
-
await page.waitForURL(/\/dashboard/);
|
|
170
|
-
|
|
171
|
-
// Should show success message or user is logged in
|
|
172
|
-
await expect(userMenuButton(page, name)).toBeVisible();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("validates email format", async ({ page }) => {
|
|
176
|
-
await page.goto("/register");
|
|
177
|
-
|
|
178
|
-
// Try invalid email
|
|
179
|
-
await page.locator('input[name="name"], #name').fill(uniqueName());
|
|
180
|
-
await page.locator('input[name="email"], #email').fill("invalid-email");
|
|
181
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
182
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill(password);
|
|
183
|
-
|
|
184
|
-
await page
|
|
185
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
186
|
-
.click();
|
|
187
|
-
|
|
188
|
-
// Should show email validation error (native or inline)
|
|
189
|
-
await expect(page.locator('input[name="email"]:invalid')).toBeVisible();
|
|
190
|
-
|
|
191
|
-
// Should not register
|
|
192
|
-
expect(page.url()).toContain("/register");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("validates password strength", async ({ page }) => {
|
|
196
|
-
await page.goto("/register");
|
|
197
|
-
|
|
198
|
-
// Try weak password
|
|
199
|
-
await page.locator('input[name="name"], #name').fill(uniqueName());
|
|
200
|
-
await page.locator('input[name="email"], #email').fill(uniqueEmail());
|
|
201
|
-
await page.locator('input[name="password"], #password').fill("123");
|
|
202
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill("123");
|
|
203
|
-
|
|
204
|
-
await page
|
|
205
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
206
|
-
.click();
|
|
207
|
-
|
|
208
|
-
// Should show password strength error
|
|
209
|
-
await expect(page.locator('input[name="password"], #password')).toHaveClass(
|
|
210
|
-
/border-destructive/
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("validates password confirmation", async ({ page }) => {
|
|
215
|
-
await page.goto("/register");
|
|
216
|
-
|
|
217
|
-
const name = uniqueName();
|
|
218
|
-
const email = uniqueEmail();
|
|
219
|
-
|
|
220
|
-
await page.locator('input[name="name"], #name').fill(name);
|
|
221
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
222
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
223
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill("different-password");
|
|
224
|
-
|
|
225
|
-
await page
|
|
226
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
227
|
-
.click();
|
|
228
|
-
|
|
229
|
-
// Should show password mismatch error
|
|
230
|
-
await expect(page.locator('input[name="confirmPassword"], #confirmPassword')).toHaveClass(
|
|
231
|
-
/border-destructive/
|
|
232
|
-
);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("prevents duplicate email registration", async ({ page, request }) => {
|
|
236
|
-
const helpers = new TestHelpers(page, request);
|
|
237
|
-
const email = uniqueEmail();
|
|
238
|
-
const name = uniqueName();
|
|
239
|
-
|
|
240
|
-
// Register first user
|
|
241
|
-
await helpers.registerAndLogin(name, email, password);
|
|
242
|
-
await page.request.post("/api/auth/clear-session");
|
|
243
|
-
|
|
244
|
-
// Try to register with same email
|
|
245
|
-
await page.goto("/register");
|
|
246
|
-
|
|
247
|
-
await page.locator('input[name="name"], #name').fill(uniqueName());
|
|
248
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
249
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
250
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill(password);
|
|
251
|
-
|
|
252
|
-
await page
|
|
253
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
254
|
-
.click();
|
|
255
|
-
|
|
256
|
-
// Should show email already exists error
|
|
257
|
-
await expect(page.getByText(/email.*exists|already.*registered|邮箱已存在|既に使用/i)).toBeVisible();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("creates organization during registration", async ({ page }) => {
|
|
261
|
-
await page.goto("/register");
|
|
262
|
-
|
|
263
|
-
const email = uniqueEmail();
|
|
264
|
-
const name = uniqueName();
|
|
265
|
-
|
|
266
|
-
await page.locator('input[name="name"], #name').fill(name);
|
|
267
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
268
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
269
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill(password);
|
|
270
|
-
|
|
271
|
-
await page
|
|
272
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
273
|
-
.click();
|
|
274
|
-
|
|
275
|
-
// Should be logged in and have access to organization
|
|
276
|
-
await expect(userMenuButton(page, name)).toBeVisible();
|
|
277
|
-
await expect(page.url()).toContain("/dashboard");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("auto-logs in after successful registration", async ({ page }) => {
|
|
281
|
-
await page.goto("/register");
|
|
282
|
-
|
|
283
|
-
const email = uniqueEmail();
|
|
284
|
-
const name = uniqueName();
|
|
285
|
-
|
|
286
|
-
await page.locator('input[name="name"], #name').fill(name);
|
|
287
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
288
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
289
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill(password);
|
|
290
|
-
|
|
291
|
-
await page
|
|
292
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
293
|
-
.click();
|
|
294
|
-
|
|
295
|
-
// Should be automatically logged in
|
|
296
|
-
await page.waitForURL(/\/dashboard/);
|
|
297
|
-
await expect(userMenuButton(page, name)).toBeVisible();
|
|
298
|
-
|
|
299
|
-
// Should be able to access protected pages without re-login
|
|
300
|
-
await page.goto("/dashboard");
|
|
301
|
-
await expect(page.url()).toContain("/dashboard");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test("shows registration success message", async ({ page }) => {
|
|
305
|
-
await page.goto("/register");
|
|
306
|
-
|
|
307
|
-
const email = uniqueEmail();
|
|
308
|
-
const name = uniqueName();
|
|
309
|
-
|
|
310
|
-
await page.locator('input[name="name"], #name').fill(name);
|
|
311
|
-
await page.locator('input[name="email"], #email').fill(email);
|
|
312
|
-
await page.locator('input[name="password"], #password').fill(password);
|
|
313
|
-
await page.locator('input[name="confirmPassword"], #confirmPassword').fill(password);
|
|
314
|
-
|
|
315
|
-
await page
|
|
316
|
-
.getByRole("button", { name: /register|sign up|signup|注册|登録/i })
|
|
317
|
-
.click();
|
|
318
|
-
|
|
319
|
-
// Verify successful registration by dashboard navigation and logged-in state
|
|
320
|
-
await page.waitForURL(/\/dashboard/);
|
|
321
|
-
await expect(userMenuButton(page, name)).toBeVisible();
|
|
322
|
-
});
|
|
323
|
-
});
|