@nexttylabs/echo 0.4.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.
Files changed (247) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
  3. package/app/api/admin/backup/route.ts +22 -4
  4. package/app/api/auth/register/handler.ts +1 -2
  5. package/lib/auth/config.ts +0 -7
  6. package/lib/db/migrations/0000_needy_leech.sql +335 -0
  7. package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
  8. package/lib/db/migrations/meta/_journal.json +2 -135
  9. package/lib/db/schema/auth.ts +0 -1
  10. package/lib/db/schema/index.ts +0 -1
  11. package/lib/portal/public-context.tsx +5 -0
  12. package/package.json +20 -1
  13. package/.changeset/README.md +0 -21
  14. package/.changeset/config.json +0 -11
  15. package/.changeset/cozy-ghosts-care.md +0 -5
  16. package/.changeset/sharp-lines-stand.md +0 -5
  17. package/.changeset/sour-doodles-eat.md +0 -5
  18. package/.changeset/tender-moose-shop.md +0 -5
  19. package/.github/pull_request_template.md +0 -13
  20. package/.github/workflows/ci.yml +0 -41
  21. package/.github/workflows/publish.yml +0 -44
  22. package/.github/workflows/release.yml +0 -73
  23. package/AGENTS.md +0 -92
  24. package/Dockerfile +0 -57
  25. package/Makefile +0 -77
  26. package/bun.lock +0 -2503
  27. package/components/portal/project-switcher.tsx +0 -20
  28. package/docker-compose.dev.yml +0 -26
  29. package/docker-compose.yml +0 -98
  30. package/docs/architecture.md +0 -259
  31. package/docs/component-inventory.md +0 -261
  32. package/docs/database-migrations.md +0 -76
  33. package/docs/development-guide.md +0 -209
  34. package/docs/e2e-user-flows.csv +0 -31
  35. package/docs/er-diagram-feedback.mmd +0 -138
  36. package/docs/er-diagram.mmd +0 -281
  37. package/docs/i18n-check-report.md +0 -296
  38. package/docs/index.md +0 -214
  39. package/docs/logic-chain.md +0 -94
  40. package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
  41. package/docs/plans/2026-01-02-user-login-design.md +0 -37
  42. package/docs/plans/2026-01-02-user-login.md +0 -437
  43. package/docs/plans/2026-01-02-user-registration-design.md +0 -47
  44. package/docs/plans/2026-01-02-user-registration.md +0 -628
  45. package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
  46. package/docs/plans/2026-01-03-roles-permissions.md +0 -266
  47. package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
  48. package/docs/plans/2026-01-05-member-removal.md +0 -186
  49. package/docs/plans/2026-01-05-organization-creation.md +0 -374
  50. package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
  51. package/docs/plans/2026-01-05-role-configuration.md +0 -441
  52. package/docs/plans/2026-01-06-file-upload-support.md +0 -804
  53. package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
  54. package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
  55. package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
  56. package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
  57. package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
  58. package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
  59. package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
  60. package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
  61. package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
  62. package/docs/plans/2026-01-09-settings-center-design.md +0 -114
  63. package/docs/plans/2026-01-09-settings-center.md +0 -948
  64. package/docs/plans/2026-01-10-organization-only-design.md +0 -66
  65. package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
  66. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
  67. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
  68. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
  69. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
  70. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
  71. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
  72. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
  73. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
  74. package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
  75. package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
  76. package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
  77. package/docs/plans/2026-01-18-changesets-design.md +0 -40
  78. package/docs/product_changes.md +0 -37
  79. package/docs/project-overview.md +0 -159
  80. package/docs/project-scan-report.json +0 -104
  81. package/docs/route-role-visibility.md +0 -51
  82. package/docs/source-tree-analysis.md +0 -150
  83. package/docs/testing/delete-project-manual-tests.md +0 -18
  84. package/docs/user-story-tracking.md +0 -191
  85. package/eslint.config.mjs +0 -19
  86. package/lib/db/migrations/.gitkeep +0 -0
  87. package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
  88. package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
  89. package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
  90. package/lib/db/migrations/0003_add_org_description.sql +0 -1
  91. package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
  92. package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
  93. package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
  94. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
  95. package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
  96. package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
  97. package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
  98. package/lib/db/migrations/0010_kind_junta.sql +0 -8
  99. package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
  100. package/lib/db/migrations/0012_giant_power_man.sql +0 -24
  101. package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
  102. package/lib/db/migrations/0014_blue_alice.sql +0 -18
  103. package/lib/db/migrations/0015_webhook_tables.sql +0 -41
  104. package/lib/db/migrations/0016_github_integration.sql +0 -30
  105. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
  106. package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
  107. package/lib/db/migrations/0018_same_spitfire.sql +0 -1
  108. package/lib/db/migrations/0019_jittery_loners.sql +0 -16
  109. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
  110. package/lib/db/migrations/meta/0001_snapshot.json +0 -553
  111. package/lib/db/migrations/meta/0002_snapshot.json +0 -560
  112. package/lib/db/migrations/meta/0003_snapshot.json +0 -650
  113. package/lib/db/migrations/meta/0004_snapshot.json +0 -852
  114. package/lib/db/migrations/meta/0005_snapshot.json +0 -900
  115. package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
  116. package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
  117. package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
  118. package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
  119. package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
  120. package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
  121. package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
  122. package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
  123. package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
  124. package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
  125. package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
  126. package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
  127. package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
  128. package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
  129. package/lib/db/schema/projects.ts +0 -145
  130. package/lib/db/schema/user-profiles.ts +0 -31
  131. package/lib/validations/projects.ts +0 -49
  132. package/next-env.d.ts +0 -6
  133. package/playwright.config.ts +0 -44
  134. package/proxy.test.ts +0 -131
  135. package/proxy.ts +0 -116
  136. package/scripts/backup-db.sh +0 -57
  137. package/scripts/backup-db.ts +0 -24
  138. package/scripts/generate-openapi.ts +0 -22
  139. package/scripts/migration-helper.ts +0 -39
  140. package/scripts/pre-deploy.ts +0 -75
  141. package/scripts/restore-db.sh +0 -60
  142. package/scripts/rollback.ts +0 -72
  143. package/scripts/seed-tags.ts +0 -48
  144. package/tests/api/feedback-bulk.test.ts +0 -47
  145. package/tests/api/feedback-by-id.test.ts +0 -67
  146. package/tests/api/feedback-comments-route-import.test.ts +0 -26
  147. package/tests/api/feedback-create.test.ts +0 -71
  148. package/tests/api/feedback-delete.test.ts +0 -160
  149. package/tests/api/feedback-filter.test.ts +0 -250
  150. package/tests/api/feedback-list.test.ts +0 -234
  151. package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
  152. package/tests/api/feedback-similar.test.ts +0 -46
  153. package/tests/api/feedback-sort.test.ts +0 -261
  154. package/tests/api/feedback-status-enum.test.ts +0 -49
  155. package/tests/api/feedback-status-filter.test.ts +0 -117
  156. package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
  157. package/tests/api/feedback.test.ts +0 -175
  158. package/tests/api/identify-jwt.test.ts +0 -25
  159. package/tests/api/invitation-accept.test.ts +0 -213
  160. package/tests/api/organization-invitations.test.ts +0 -186
  161. package/tests/api/organization-members-list.test.ts +0 -79
  162. package/tests/api/organization-members.test.ts +0 -340
  163. package/tests/api/organizations.test.ts +0 -149
  164. package/tests/api/register.test.ts +0 -112
  165. package/tests/api/upload.test.ts +0 -103
  166. package/tests/api/vote.test.ts +0 -82
  167. package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
  168. package/tests/app/admin-feedback-list-page.test.tsx +0 -25
  169. package/tests/app/admin-feedback-new-page.test.tsx +0 -25
  170. package/tests/app/health-route-helpers.test.ts +0 -27
  171. package/tests/app/login-page.test.ts +0 -26
  172. package/tests/app/portal-page.test.ts +0 -29
  173. package/tests/app/project-portal-overview.test.tsx +0 -25
  174. package/tests/app/widget-page-import.test.ts +0 -25
  175. package/tests/components/create-post-dialog-defaults.test.ts +0 -43
  176. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
  177. package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
  178. package/tests/components/feedback/feedback-detail.test.tsx +0 -25
  179. package/tests/components/feedback/feedback-stats.test.tsx +0 -49
  180. package/tests/components/feedback-bulk-actions.test.tsx +0 -39
  181. package/tests/components/feedback-i18n-keys.test.ts +0 -70
  182. package/tests/components/feedback-list-controls-compile.test.ts +0 -25
  183. package/tests/components/feedback-list-controls.test.tsx +0 -204
  184. package/tests/components/feedback-list-item.test.tsx +0 -67
  185. package/tests/components/landing/hero.test.tsx +0 -46
  186. package/tests/components/layout/language-switcher.test.tsx +0 -25
  187. package/tests/components/layout/sidebar.test.tsx +0 -157
  188. package/tests/components/login-form.test.ts +0 -25
  189. package/tests/components/organization-form.test.ts +0 -32
  190. package/tests/components/organization-switcher.test.ts +0 -25
  191. package/tests/components/pagination.test.tsx +0 -43
  192. package/tests/components/portal-overview.test.tsx +0 -25
  193. package/tests/components/profile-form.test.tsx +0 -139
  194. package/tests/components/role-selector.test.ts +0 -31
  195. package/tests/components/status-chart.test.tsx +0 -90
  196. package/tests/e2e/auth.e2e.ts +0 -323
  197. package/tests/e2e/feedback-actions.e2e.ts +0 -471
  198. package/tests/e2e/feedback-attachment.e2e.ts +0 -168
  199. package/tests/e2e/feedback-customer.e2e.ts +0 -226
  200. package/tests/e2e/feedback-management.e2e.ts +0 -565
  201. package/tests/e2e/feedback-submit.e2e.ts +0 -133
  202. package/tests/e2e/feedback-view.e2e.ts +0 -297
  203. package/tests/e2e/fixtures/test-data.ts +0 -235
  204. package/tests/e2e/health-check.e2e.ts +0 -230
  205. package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
  206. package/tests/e2e/helpers/test-utils.ts +0 -298
  207. package/tests/e2e/integration-placeholders.e2e.ts +0 -199
  208. package/tests/e2e/organization.e2e.ts +0 -292
  209. package/tests/e2e/permissions.e2e.ts +0 -424
  210. package/tests/e2e/project-widget.e2e.ts +0 -63
  211. package/tests/feedback/filters.test.ts +0 -29
  212. package/tests/hooks/use-permissions.test.ts +0 -52
  213. package/tests/lib/ai/classifier.test.ts +0 -104
  214. package/tests/lib/ai/duplicate-detector.test.ts +0 -234
  215. package/tests/lib/attachments-schema.test.ts +0 -30
  216. package/tests/lib/auth/session.test.ts +0 -49
  217. package/tests/lib/auth-client.test.ts +0 -37
  218. package/tests/lib/auth-config.test.ts +0 -26
  219. package/tests/lib/feedback-prefill.test.ts +0 -52
  220. package/tests/lib/feedback-processor.test.ts +0 -41
  221. package/tests/lib/feedback-schema.test.ts +0 -33
  222. package/tests/lib/file-validator.test.ts +0 -48
  223. package/tests/lib/get-feedback-by-id.test.ts +0 -37
  224. package/tests/lib/invitations.test.ts +0 -35
  225. package/tests/lib/login-schema.test.ts +0 -36
  226. package/tests/lib/org-context.test.ts +0 -95
  227. package/tests/lib/organization-access.test.ts +0 -44
  228. package/tests/lib/organization-member-role-schema.test.ts +0 -41
  229. package/tests/lib/permissions.test.ts +0 -88
  230. package/tests/lib/portal-analytics.test.ts +0 -25
  231. package/tests/lib/portal-contributors.test.ts +0 -25
  232. package/tests/lib/portal-copy.test.ts +0 -27
  233. package/tests/lib/portal-i18n.test.ts +0 -30
  234. package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
  235. package/tests/lib/portal-modules.test.ts +0 -25
  236. package/tests/lib/portal-seo.test.ts +0 -25
  237. package/tests/lib/portal-sharing.test.ts +0 -25
  238. package/tests/lib/portal-sorting.test.ts +0 -25
  239. package/tests/lib/portal-theme.test.ts +0 -25
  240. package/tests/lib/rate-limit.test.ts +0 -142
  241. package/tests/lib/resolve-locale.test.ts +0 -34
  242. package/tests/lib/services/backup.test.ts +0 -145
  243. package/tests/lib/user-organizations.test.ts +0 -42
  244. package/tests/lib/user-role-schema.test.ts +0 -33
  245. package/tests/lib/user-schema.test.ts +0 -25
  246. package/tests/setup.ts +0 -74
  247. package/vercel.json +0 -4
@@ -1,565 +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, uniqueEmail, uniqueName, TestHelpers } from "./helpers/test-utils";
19
- import { TestDataManager, createTestFeedback } from "./fixtures/test-data";
20
-
21
- test.describe("E2E-UF-007: View feedback list", () => {
22
- let helpers: TestHelpers;
23
- let testDataManager: TestDataManager;
24
- let organizationId: string;
25
-
26
- test.beforeEach(async ({ page, request }) => {
27
- helpers = new TestHelpers(page, request);
28
-
29
- const email = uniqueEmail();
30
- const name = uniqueName();
31
- const password = "StrongPass123!";
32
-
33
-
34
- await helpers.registerAndLogin(name, email, password);
35
- testDataManager = new TestDataManager(page.request);
36
-
37
- // Get organization ID from registration response (authenticated request context isn't shared)
38
- organizationId = helpers.getOrganizationId();
39
-
40
- // Create test feedback
41
- for (let i = 0; i < 5; i++) {
42
- await testDataManager.createFeedback({
43
- ...createTestFeedback({
44
- title: `Test Feedback ${i + 1}`,
45
- description: `Description for feedback ${i + 1}`,
46
- }),
47
- organizationId,
48
- });
49
- }
50
- });
51
-
52
- test("loads feedback list with default view", async ({ page }) => {
53
- await page.goto("/admin/feedback");
54
-
55
- // Should show feedback list
56
- await expect(page.getByRole("heading", { name: /反馈|feedback/i })).toBeVisible();
57
-
58
- // Should show feedback items
59
- const feedbackItems = page.locator('[data-testid="feedback-item"], .feedback-item');
60
- await expect(feedbackItems.first()).toBeVisible();
61
-
62
- // Should show key fields
63
- await expect(page.getByText("Test Feedback 1")).toBeVisible();
64
- await expect(page.getByText("Description for feedback 1")).toBeVisible();
65
- });
66
-
67
- test("shows pagination if feedback exceeds page limit", async ({ page }) => {
68
- // Create more feedback to trigger pagination
69
- for (let i = 5; i < 25; i++) {
70
- await testDataManager.createFeedback({
71
- ...createTestFeedback({
72
- title: `Extra Feedback ${i}`,
73
- description: `Extra description ${i}`,
74
- }),
75
- organizationId,
76
- });
77
- }
78
-
79
- await page.goto("/admin/feedback");
80
-
81
- // Should see pagination controls
82
- const pagination = page.locator('[data-testid="pagination"], .pagination');
83
- if (await pagination.isVisible()) {
84
- await expect(pagination).toBeVisible();
85
-
86
- // Should show page numbers
87
- await expect(page.getByRole("button", { name: /2/i })).toBeVisible();
88
- }
89
- });
90
-
91
- test("displays feedback metadata in list", async ({ page }) => {
92
- await page.goto("/admin/feedback");
93
-
94
- // Check for metadata columns
95
- const statusColumn = page.locator('[data-testid="status-column"], th:has-text("Status")');
96
- const dateColumn = page.locator('[data-testid="date-column"], th:has-text("Date")');
97
- const votesColumn = page.locator('[data-testid="votes-column"], th:has-text("Votes")');
98
-
99
- if (await statusColumn.isVisible()) {
100
- await expect(statusColumn).toBeVisible();
101
- }
102
-
103
- if (await dateColumn.isVisible()) {
104
- await expect(dateColumn).toBeVisible();
105
- }
106
-
107
- if (await votesColumn.isVisible()) {
108
- await expect(votesColumn).toBeVisible();
109
- }
110
- });
111
- });
112
-
113
- test.describe("E2E-UF-008: Filter feedback by status", () => {
114
- let helpers: TestHelpers;
115
- let testDataManager: TestDataManager;
116
- let organizationId: string;
117
-
118
- test.beforeEach(async ({ page, request }) => {
119
- helpers = new TestHelpers(page, request);
120
-
121
- const email = uniqueEmail();
122
- const name = uniqueName();
123
- const password = "StrongPass123!";
124
-
125
-
126
- await helpers.registerAndLogin(name, email, password);
127
- testDataManager = new TestDataManager(page.request);
128
-
129
- // Get organization ID from registration response (authenticated request context isn't shared)
130
- organizationId = helpers.getOrganizationId();
131
-
132
- // Create feedback with different statuses
133
- await testDataManager.createFeedback({
134
- ...createTestFeedback({ title: "Open Feedback" }),
135
- organizationId,
136
- });
137
-
138
- await testDataManager.createFeedback({
139
- ...createTestFeedback({ title: "In Progress Feedback" }),
140
- organizationId,
141
- });
142
-
143
- await testDataManager.createFeedback({
144
- ...createTestFeedback({ title: "Completed Feedback" }),
145
- organizationId,
146
- });
147
- });
148
-
149
- test("filters feedback by status successfully", async ({ page }) => {
150
- await page.goto("/admin/feedback");
151
-
152
- // Find status filter
153
- const statusFilter = page.locator('[data-testid="status-filter"], select[name="status"]');
154
- if (await statusFilter.isVisible()) {
155
- // Filter by "Open" status
156
- await statusFilter.click();
157
- await page.getByRole("option", { name: /open/i }).click();
158
-
159
- // Wait for filter to apply
160
- await page.waitForTimeout(500);
161
-
162
- // Should only show open feedback
163
- await expect(page.getByText("Open Feedback")).toBeVisible();
164
- await expect(page.getByText("In Progress Feedback")).not.toBeVisible();
165
- await expect(page.getByText("Completed Feedback")).not.toBeVisible();
166
- } else {
167
- // Alternative: click on status tabs or buttons
168
- const openTab = page.locator('[data-testid="status-tab-open"], button:has-text("Open")');
169
- if (await openTab.isVisible()) {
170
- await openTab.click();
171
- await expect(page.getByText("Open Feedback")).toBeVisible();
172
- }
173
- }
174
- });
175
-
176
- test("shows active filter indicator", async ({ page }) => {
177
- await page.goto("/admin/feedback");
178
-
179
- const statusFilter = page.locator('[data-testid="status-filter"], select[name="status"]');
180
- if (await statusFilter.isVisible()) {
181
- await statusFilter.click();
182
- await page.getByRole("option", { name: /in progress/i }).click();
183
-
184
- // Should show active filter
185
- await expect(page.getByText(/filter.*in progress|status.*in progress/i)).toBeVisible();
186
-
187
- // Should show clear filter option
188
- const clearFilter = page.locator('[data-testid="clear-filter"], button:has-text("Clear")');
189
- if (await clearFilter.isVisible()) {
190
- await expect(clearFilter).toBeVisible();
191
- }
192
- }
193
- });
194
-
195
- test("clears filter and shows all feedback", async ({ page }) => {
196
- await page.goto("/admin/feedback");
197
-
198
- const statusFilter = page.locator('[data-testid="status-filter"], select[name="status"]');
199
- if (await statusFilter.isVisible()) {
200
- // Apply filter
201
- await statusFilter.click();
202
- await page.getByRole("option", { name: /open/i }).click();
203
- await page.waitForTimeout(500);
204
-
205
- // Clear filter
206
- const clearFilter = page.locator('[data-testid="clear-filter"], button:has-text("Clear")');
207
- if (await clearFilter.isVisible()) {
208
- await clearFilter.click();
209
- } else {
210
- // Or select "All" option
211
- await statusFilter.click();
212
- await page.getByRole("option", { name: /all/i }).click();
213
- }
214
-
215
- // Should show all feedback again
216
- await expect(page.getByText("Open Feedback")).toBeVisible();
217
- await expect(page.getByText("In Progress Feedback")).toBeVisible();
218
- await expect(page.getByText("Completed Feedback")).toBeVisible();
219
- }
220
- });
221
- });
222
-
223
- test.describe("E2E-UF-009: Sort feedback by votes and date", () => {
224
- let helpers: TestHelpers;
225
- let testDataManager: TestDataManager;
226
- let organizationId: string;
227
-
228
- test.beforeEach(async ({ page, request }) => {
229
- helpers = new TestHelpers(page, request);
230
-
231
- const email = uniqueEmail();
232
- const name = uniqueName();
233
- const password = "StrongPass123!";
234
-
235
-
236
- await helpers.registerAndLogin(name, email, password);
237
- testDataManager = new TestDataManager(page.request);
238
-
239
- // Get organization ID from registration response (authenticated request context isn't shared)
240
- organizationId = helpers.getOrganizationId();
241
-
242
- // Create feedback with different dates
243
- await testDataManager.createFeedback({
244
- ...createTestFeedback({ title: "Oldest Feedback" }),
245
- organizationId,
246
- });
247
-
248
- // Wait a bit to ensure different timestamps
249
- await new Promise(resolve => setTimeout(resolve, 100));
250
-
251
- await testDataManager.createFeedback({
252
- ...createTestFeedback({ title: "Newest Feedback" }),
253
- organizationId,
254
- });
255
- });
256
-
257
- test("sorts feedback by vote count", async ({ page }) => {
258
- await page.goto("/admin/feedback");
259
-
260
- // Find sort dropdown
261
- const sortSelect = page.locator('[data-testid="sort-select"], select[name="sort"]');
262
- if (await sortSelect.isVisible()) {
263
- // Sort by votes
264
- await sortSelect.click();
265
- await page.getByRole("option", { name: /votes/i }).click();
266
-
267
- // Wait for sort to apply
268
- await page.waitForTimeout(500);
269
-
270
- // Should show sorted indicator
271
- await expect(page.getByText(/sorted.*votes|sort.*votes/i)).toBeVisible();
272
- } else {
273
- // Alternative: click on column headers
274
- const votesHeader = page.locator('[data-testid="votes-header"], th:has-text("Votes")');
275
- if (await votesHeader.isVisible()) {
276
- await votesHeader.click();
277
- await expect(page.getByText(/sorted.*votes/i)).toBeVisible();
278
- }
279
- }
280
- });
281
-
282
- test("sorts feedback by creation date", async ({ page }) => {
283
- await page.goto("/admin/feedback");
284
-
285
- const sortSelect = page.locator('[data-testid="sort-select"], select[name="sort"]');
286
- if (await sortSelect.isVisible()) {
287
- // Sort by date (newest first)
288
- await sortSelect.click();
289
- await page.getByRole("option", { name: /date|newest/i }).click();
290
-
291
- await page.waitForTimeout(500);
292
-
293
- // Newest should be first
294
- const firstItem = page.locator('[data-testid="feedback-item"]').first();
295
- await expect(firstItem.getByText("Newest Feedback")).toBeVisible();
296
-
297
- // Sort by date (oldest first)
298
- await sortSelect.click();
299
- await page.getByRole("option", { name: /oldest/i }).click();
300
-
301
- await page.waitForTimeout(500);
302
-
303
- // Oldest should be first
304
- await expect(firstItem.getByText("Oldest Feedback")).toBeVisible();
305
- }
306
- });
307
-
308
- test("toggles sort direction", async ({ page }) => {
309
- await page.goto("/admin/feedback");
310
-
311
- // Click on date header to sort
312
- const dateHeader = page.locator('[data-testid="date-header"], th:has-text("Date")');
313
- if (await dateHeader.isVisible()) {
314
- await dateHeader.click();
315
-
316
- // Should show sort indicator
317
- await expect(dateHeader.locator('[data-testid="sort-indicator"], .sort-indicator')).toBeVisible();
318
-
319
- // Click again to reverse
320
- await dateHeader.click();
321
-
322
- // Sort direction should change
323
- const sortIndicator = dateHeader.locator('[data-testid="sort-indicator"]');
324
- await expect(sortIndicator).toHaveClass(/desc|asc/);
325
- }
326
- });
327
- });
328
-
329
- test.describe("E2E-UF-010: View feedback details", () => {
330
- let helpers: TestHelpers;
331
- let testDataManager: TestDataManager;
332
- let organizationId: string;
333
- let feedbackId: number;
334
-
335
- test.beforeEach(async ({ page, request }) => {
336
- helpers = new TestHelpers(page, request);
337
-
338
- const email = uniqueEmail();
339
- const name = uniqueName();
340
- const password = "StrongPass123!";
341
-
342
-
343
- await helpers.registerAndLogin(name, email, password);
344
- testDataManager = new TestDataManager(page.request);
345
-
346
- // Get organization ID from registration response (authenticated request context isn't shared)
347
- organizationId = helpers.getOrganizationId();
348
-
349
- // Create test feedback
350
- const feedback = await testDataManager.createFeedback({
351
- ...createTestFeedback({
352
- title: "Detailed Feedback",
353
- description: "This is a detailed description with multiple lines.\n\nIt has important information.",
354
- type: "bug",
355
- priority: "high",
356
- }),
357
- organizationId,
358
- });
359
-
360
- feedbackId = feedback.feedbackId;
361
- });
362
-
363
- test("shows complete feedback information", async ({ page }) => {
364
- await page.goto(`/admin/feedback/${feedbackId}`);
365
-
366
- // Should show all feedback fields
367
- await expect(page.getByRole("heading", { name: "Detailed Feedback" })).toBeVisible();
368
- await expect(page.getByText(/This is a detailed description/)).toBeVisible();
369
-
370
- // Should show metadata
371
- await expect(page.getByText("Bug")).toBeVisible();
372
- await expect(page.getByText(/优先级:\s*高/)).toBeVisible();
373
-
374
- // Should show status selector
375
- await expect(page.getByRole("combobox")).toBeVisible();
376
-
377
- // Should show timestamps
378
- await expect(page.getByText("创建时间")).toBeVisible();
379
- await expect(page.getByText("更新时间")).toBeVisible();
380
- });
381
-
382
- test("navigates from list to detail view", async ({ page }) => {
383
- await page.goto("/admin/feedback");
384
-
385
- // Click on feedback item
386
- const feedbackItem = page.locator('[data-testid="feedback-item"]:has-text("Detailed Feedback")');
387
- await feedbackItem.click();
388
-
389
- // Should navigate to detail page
390
- await page.waitForURL(/\/admin\/feedback\/[^\/]+$/);
391
- await expect(page.getByRole("heading", { name: "Detailed Feedback" })).toBeVisible();
392
- });
393
-
394
- test("shows edit and action buttons for authorized users", async ({ page }) => {
395
- await page.goto(`/admin/feedback/${feedbackId}`);
396
-
397
- // Should see action buttons
398
- const editButton = page.locator('button').filter({ hasText: /edit/i });
399
- if (await editButton.isVisible()) {
400
- await expect(editButton).toBeVisible();
401
- }
402
-
403
- const deleteButton = page.locator('button').filter({ hasText: /delete/i });
404
- if (await deleteButton.isVisible()) {
405
- await expect(deleteButton).toBeVisible();
406
- }
407
-
408
- const statusSelect = page.locator('[data-testid="status-select"]');
409
- if (await statusSelect.isVisible()) {
410
- await expect(statusSelect).toBeVisible();
411
- }
412
- });
413
-
414
- test("displays feedback activity history", async ({ page }) => {
415
- await page.goto(`/admin/feedback/${feedbackId}`);
416
-
417
- // Should show activity timeline
418
- const activityHistory = page.locator('[data-testid="activity-history"], .activity-timeline');
419
- if (await activityHistory.isVisible()) {
420
- await expect(activityHistory).toBeVisible();
421
-
422
- // Should show creation activity
423
- await expect(page.getByText(/created|submitted/i)).toBeVisible();
424
- }
425
- });
426
- });
427
-
428
- test.describe("E2E-UF-011: Modify feedback status", () => {
429
- let helpers: TestHelpers;
430
- let testDataManager: TestDataManager;
431
- let organizationId: string;
432
- let feedbackId: number;
433
-
434
- test.beforeEach(async ({ page, request }) => {
435
- helpers = new TestHelpers(page, request);
436
-
437
- const email = uniqueEmail();
438
- const name = uniqueName();
439
- const password = "StrongPass123!";
440
-
441
-
442
- await helpers.registerAndLogin(name, email, password);
443
- testDataManager = new TestDataManager(page.request);
444
-
445
- // Get organization ID from registration response (authenticated request context isn't shared)
446
- organizationId = helpers.getOrganizationId();
447
-
448
- // Create test feedback
449
- const feedback = await testDataManager.createFeedback({
450
- ...createTestFeedback({
451
- title: "Status Test Feedback",
452
- description: "Testing status changes",
453
- }),
454
- organizationId,
455
- });
456
-
457
- feedbackId = feedback.feedbackId;
458
- });
459
-
460
- test("changes feedback status successfully", async ({ page }) => {
461
- await page.goto(`/admin/feedback/${feedbackId}`);
462
-
463
- // Select new status
464
- const statusSelect = page.getByRole("combobox");
465
- await statusSelect.click();
466
- await page.getByRole("option", { name: /处理中/ }).click();
467
-
468
- // Verify status changed
469
- await expect(statusSelect).toContainText("处理中");
470
- });
471
-
472
- test("shows confirmation before status change", async ({ page }) => {
473
- await page.goto(`/admin/feedback/${feedbackId}`);
474
-
475
- const statusSelect = page.getByRole("combobox");
476
- await statusSelect.click();
477
- await page.getByRole("option", { name: /已完成/ }).click();
478
-
479
- // Might show confirmation dialog
480
- const confirmDialog = page.locator('[data-testid="confirm-dialog"], .modal');
481
- if (await confirmDialog.isVisible()) {
482
- await expect(confirmDialog.getByText(/change status/i)).toBeVisible();
483
- await page.getByRole("button", { name: /confirm|yes/i }).click();
484
- }
485
-
486
- // Verify status changed
487
- await expect(statusSelect).toContainText("已完成");
488
- });
489
-
490
- test("syncs status change across list and detail views", async ({ page }) => {
491
- // Change status in detail view
492
- await page.goto(`/admin/feedback/${feedbackId}`);
493
-
494
- const statusSelect = page.getByRole("combobox");
495
- await statusSelect.click();
496
- await page.getByRole("option", { name: /处理中/ }).click();
497
-
498
- // Navigate back to list
499
- await page.goto("/admin/feedback");
500
-
501
- // Should show updated status in list
502
- const feedbackItem = page.locator('[data-testid="feedback-item"]:has-text("Status Test Feedback")');
503
- await expect(feedbackItem.getByText(/处理中/)).toBeVisible();
504
-
505
- // Go back to detail
506
- await feedbackItem.click();
507
-
508
- // Should still show updated status
509
- await expect(page.getByRole("combobox")).toContainText("处理中");
510
- });
511
-
512
- test("requires permission to change status", async ({ page, request }) => {
513
- // Create a member user without permission
514
- const memberEmail = uniqueEmail();
515
- const memberName = uniqueName();
516
-
517
- await request.post("/api/auth/register", {
518
- data: {
519
- name: memberName,
520
- email: memberEmail,
521
- password: "TestPass123!",
522
- },
523
- });
524
-
525
- // Invite member to the current organization with a restricted role
526
- const inviteResponse = await page.request.post(
527
- `/api/organizations/${organizationId}/invitations`,
528
- { data: { email: memberEmail, role: "customer" } },
529
- );
530
- expect(inviteResponse.ok()).toBeTruthy();
531
- const inviteJson = await inviteResponse.json();
532
- const inviteToken = inviteJson?.data?.token as string | undefined;
533
- expect(inviteToken).toBeTruthy();
534
-
535
- // Logout admin
536
- await page.context().clearCookies();
537
-
538
- // Login as member
539
- const memberHelpers = new TestHelpers(page, page.request);
540
- await memberHelpers.login(memberEmail, "TestPass123!");
541
-
542
- // Accept invitation to join the admin's organization
543
- const acceptResponse = await page.request.post("/api/invitations/accept", {
544
- data: { token: inviteToken },
545
- });
546
- expect(acceptResponse.ok()).toBeTruthy();
547
-
548
- // Ensure org context points to the admin organization
549
- await page.evaluate((orgId) => {
550
- document.cookie = `orgId=${orgId};path=/;max-age=2592000;samesite=lax`;
551
- }, organizationId);
552
-
553
- // Try to change status
554
- await page.goto(`/admin/feedback/${feedbackId}`);
555
-
556
- const statusSelect = page.getByRole("combobox");
557
-
558
- // Should be disabled or not visible
559
- if (await statusSelect.isVisible()) {
560
- await expect(statusSelect).toBeDisabled();
561
- } else {
562
- await expect(statusSelect).toHaveCount(0);
563
- }
564
- });
565
- });
@@ -1,133 +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 } from "@playwright/test";
19
-
20
- function uniqueEmail() {
21
- return `e2e+${Date.now()}@example.com`;
22
- }
23
-
24
- function uniqueTitle() {
25
- return `E2E feedback ${Date.now()}`;
26
- }
27
-
28
- function uniqueName() {
29
- return `E2E User ${Date.now()}`;
30
- }
31
-
32
- test.describe("E2E-UF-001: Submit feedback with basic fields, type, and priority", () => {
33
- let slug: string;
34
- const password = "StrongPass123!";
35
-
36
- test.beforeEach(async ({ page }) => {
37
- const email = uniqueEmail();
38
- const name = uniqueName();
39
-
40
- const register = await page.request.post("/api/auth/register", {
41
- data: { name, email, password },
42
- });
43
-
44
- expect(register.ok()).toBeTruthy();
45
- const json = await register.json();
46
- slug = json?.data?.organization?.slug;
47
- expect(slug).toBeTruthy();
48
- });
49
-
50
- test("submits feedback with title, description, type, and priority", async ({
51
- page,
52
- }) => {
53
- const title = uniqueTitle();
54
- const description = "This is an E2E submission with type and priority.";
55
-
56
- // Navigate to the portal
57
- await page.goto(`/${slug}`);
58
-
59
- // Open feedback form
60
- await page.getByRole("button", { name: "Submit Feedback" }).click();
61
-
62
- // Fill basic fields
63
- await page.getByLabel("Title").fill(title);
64
- await page.getByLabel("Description").fill(description);
65
-
66
- // Select feedback type (e.g., Feature Request)
67
- const typeSelect = page.getByLabel("Type");
68
- if (await typeSelect.isVisible()) {
69
- await typeSelect.click();
70
- await page.getByRole("option", { name: /feature/i }).click();
71
- }
72
-
73
- // Select priority (e.g., High)
74
- const prioritySelect = page.getByLabel("Priority");
75
- if (await prioritySelect.isVisible()) {
76
- await prioritySelect.click();
77
- await page.getByRole("option", { name: /high/i }).click();
78
- }
79
-
80
- // Submit the form
81
- await page.getByRole("button", { name: "Create Post" }).click();
82
-
83
- // Assert: feedback detail page shows the title
84
- await expect(page.getByRole("heading", { name: title })).toBeVisible();
85
-
86
- // Assert: we stay on the portal list page after submit
87
- const currentUrl = page.url();
88
- expect(currentUrl).toContain(`/${slug}`);
89
-
90
- // Navigate to the new feedback detail page via the list
91
- const listHeading = page.getByRole("heading", { name: title });
92
- await expect(listHeading).toBeVisible();
93
- await listHeading.click();
94
- await expect(
95
- page.getByRole("heading", { name: title, level: 1 })
96
- ).toBeVisible();
97
- expect(page.url()).toContain(`/${slug}/feedback/`);
98
- });
99
-
100
- test("shows success message after submission", async ({ page }) => {
101
- const title = uniqueTitle();
102
-
103
- await page.goto(`/${slug}`);
104
- await page.getByRole("button", { name: "Submit Feedback" }).click();
105
-
106
- await page.getByLabel("Title").fill(title);
107
- await page.getByLabel("Description").fill("Testing success message.");
108
- await page.getByRole("button", { name: "Create Post" }).click();
109
-
110
- // Assert: the new feedback appears in the list after reload
111
- await expect(page.getByRole("heading", { name: title })).toBeVisible();
112
- });
113
-
114
- test("new feedback appears in the feedback list", async ({ page }) => {
115
- const title = uniqueTitle();
116
-
117
- await page.goto(`/${slug}`);
118
- await page.getByRole("button", { name: "Submit Feedback" }).click();
119
-
120
- await page.getByLabel("Title").fill(title);
121
- await page.getByLabel("Description").fill("Testing list visibility.");
122
- await page.getByRole("button", { name: "Create Post" }).click();
123
-
124
- // Wait for navigation to detail or list
125
- await expect(page.getByRole("heading", { name: title })).toBeVisible();
126
-
127
- // Navigate back to the portal list
128
- await page.goto(`/${slug}`);
129
-
130
- // Assert: the new feedback is visible in the list
131
- await expect(page.getByText(title)).toBeVisible();
132
- });
133
- });