Skip to content

Commit f20a5d4

Browse files
authored
fix: Handle default values for text long / link result fields (#67)
* tests: Add e2e tests for result fields * tests: Enhance e2e tests for user updates and add new tests for result fields * refactor: update component styles for improved responsiveness - Adjusted max-width and minimum height styles in AddResultFieldModal, EditResultFieldModal, and RenderField components for better layout consistency across different screen sizes. - Modified width of the DatePickerField button for a more uniform appearance. * Revert "refactor: update component styles for improved responsiveness" This reverts commit c34d5ea. * fix: enhance AddCaseField and AddResultField components with type switching logic - Added useRef to track previous field type names in AddCaseFieldModal and AddResultFieldModal. - Implemented logic to clear default values when switching between field types, improving user experience. - Updated input types dynamically based on selected field type for better validation and usability. - Integrated TipTapEditor for "Text Long" field type to enhance text editing capabilities. * Revert "fix: enhance AddCaseField and AddResultField components with type switching logic" This reverts commit 2e780f4. * Reapply "fix: enhance AddCaseField and AddResultField components with type switching logic" This reverts commit 224091e. * fix: improve folder selection logic in e2e tests - Updated the folder selection process in the RepositoryPage to include retry logic and ensure the correct folder is selected. - Enhanced URL verification in tests to check for a node parameter instead of specific folder IDs, improving test robustness. - Adjusted worker settings in Playwright configuration for better stability during tests. * fix: set defauilt global notifications to in-app/email for new instances refactor: enhance test stability and readability with data-testid attributes - Updated e2e tests to use stable data-testid selectors for user menu and theme submenu interactions, improving test reliability. - Refactored version history tests to utilize data-testid attributes for diff indicators, ensuring clearer visibility checks. - Modified project creation logic in seed script to include a unique identifier, enhancing uniqueness across parallel test runs. - Added notification settings seeding to ensure consistent application configuration.
1 parent 19b923c commit f20a5d4

File tree

15 files changed

+1305
-59
lines changed

15 files changed

+1305
-59
lines changed

testplanit/app/[locale]/admin/fields/AddCaseField.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22
/* eslint-disable react-hooks/incompatible-library */
3-
import React, { useState, useEffect, useMemo } from "react";
3+
import React, { useState, useEffect, useMemo, useRef } from "react";
44
import {
55
useCreateCaseFields,
66
useFindManyCaseFieldTypes,
@@ -164,6 +164,7 @@ export function AddCaseFieldModal({
164164
const [lastId, setLastId] = useState(0);
165165
const [error, setError] = useState<string | null>(null);
166166
const [defaultItem, setDefaultItem] = useState<number | null>(null);
167+
const previousTypeNameRef = useRef<string | undefined | null>(undefined);
167168

168169
const applyOptionOrder = (options: FieldOptions[]): FieldOptions[] =>
169170
options.map((option, index) => ({ ...option, order: index }));
@@ -439,7 +440,20 @@ export function AddCaseFieldModal({
439440

440441
useEffect(() => {
441442
const foundType = types?.find((type) => type.id.toString() === typeId);
442-
setSelectedTypeName(foundType?.type);
443+
const newTypeName = foundType?.type;
444+
445+
// Clear defaultValue when switching field types
446+
if (
447+
previousTypeNameRef.current &&
448+
newTypeName &&
449+
previousTypeNameRef.current !== newTypeName
450+
) {
451+
setValue("defaultValue", "");
452+
}
453+
454+
previousTypeNameRef.current = newTypeName;
455+
setSelectedTypeName(newTypeName);
456+
443457
if (foundType && foundType.options) {
444458
try {
445459
const parsedOptions =
@@ -454,7 +468,7 @@ export function AddCaseFieldModal({
454468
} else {
455469
setSelectedTypeOptions(null);
456470
}
457-
}, [types, typeId]);
471+
}, [types, typeId, setValue]);
458472

459473
const renderOptions = (options: any) => {
460474
const currentType = types?.find(

testplanit/app/[locale]/admin/fields/AddResultField.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22
/* eslint-disable react-hooks/incompatible-library */
3-
import React, { useState, useEffect, useMemo } from "react";
3+
import React, { useState, useEffect, useMemo, useRef } from "react";
44
import {
55
useCreateResultFields,
66
useFindManyCaseFieldTypes,
@@ -19,6 +19,8 @@ import { Button } from "@/components/ui/button";
1919
import { Input } from "@/components/ui/input";
2020
import { Label } from "@/components/ui/label";
2121
import { DraggableList } from "@/components/DraggableFieldOptions";
22+
import TipTapEditor from "@/components/tiptap/TipTapEditor";
23+
import { emptyEditorContent } from "~/app/constants";
2224

2325
import { CirclePlus, Ellipsis } from "lucide-react";
2426

@@ -155,6 +157,7 @@ export function AddResultFieldModal({
155157
const [lastId, setLastId] = useState(0);
156158
const [error, setError] = useState<string | null>(null);
157159
const [defaultItem, setDefaultItem] = useState<number | null>(null);
160+
const previousTypeNameRef = useRef<string | undefined | null>(undefined);
158161

159162
const applyOptionOrder = (options: FieldOptions[]): FieldOptions[] =>
160163
options.map((option, index) => ({ ...option, order: index }));
@@ -432,7 +435,20 @@ export function AddResultFieldModal({
432435

433436
useEffect(() => {
434437
const foundType = types?.find((type) => type.id.toString() === typeId);
435-
setSelectedTypeName(foundType?.type);
438+
const newTypeName = foundType?.type;
439+
440+
// Clear defaultValue when switching field types
441+
if (
442+
previousTypeNameRef.current &&
443+
newTypeName &&
444+
previousTypeNameRef.current !== newTypeName
445+
) {
446+
setValue("defaultValue", "");
447+
}
448+
449+
previousTypeNameRef.current = newTypeName;
450+
setSelectedTypeName(newTypeName);
451+
436452
if (foundType && foundType.options) {
437453
try {
438454
const parsedOptions =
@@ -447,7 +463,7 @@ export function AddResultFieldModal({
447463
} else {
448464
setSelectedTypeOptions(null);
449465
}
450-
}, [types, typeId]);
466+
}, [types, typeId, setValue]);
451467

452468
const renderOptions = (options: any) => {
453469
const currentType = types?.find(
@@ -527,10 +543,32 @@ export function AddResultFieldModal({
527543
/>
528544
</div>
529545
</div>
546+
) : option.key === "defaultValue" &&
547+
selectedTypeName === "Text Long" ? (
548+
<div
549+
className="ring-2 ring-muted rounded-lg min-h-[200px]"
550+
data-testid={`result-field-${option.key}`}
551+
>
552+
<TipTapEditor
553+
content={(() => {
554+
try {
555+
return field.value
556+
? JSON.parse(field.value as string)
557+
: emptyEditorContent;
558+
} catch {
559+
return emptyEditorContent;
560+
}
561+
})()}
562+
onUpdate={(content) => {
563+
field.onChange(JSON.stringify(content));
564+
}}
565+
className="min-h-[200px]"
566+
/>
567+
</div>
530568
) : option.key === "defaultValue" ? (
531569
<Input
532570
{...field}
533-
type="text"
571+
type={selectedTypeName === "Link" ? "url" : "text"}
534572
onChange={field.onChange}
535573
value={(field.value ?? "") as string}
536574
data-testid={`result-field-${option.key}`}

testplanit/app/[locale]/admin/fields/EditResultField.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { ExtendedResultFields } from "./resultFieldColumns";
1919
import { Button } from "@/components/ui/button";
2020
import { Input } from "@/components/ui/input";
2121
import { DraggableList } from "@/components/DraggableFieldOptions";
22+
import TipTapEditor from "@/components/tiptap/TipTapEditor";
23+
import { emptyEditorContent } from "~/app/constants";
2224

2325
import { SquarePen } from "lucide-react";
2426

@@ -473,10 +475,29 @@ export function EditResultFieldModal({
473475
/>
474476
</div>
475477
</div>
478+
) : option.key === "defaultValue" &&
479+
selectedTypeName === "Text Long" ? (
480+
<div className="ring-2 ring-muted rounded-lg min-h-[200px]">
481+
<TipTapEditor
482+
content={(() => {
483+
try {
484+
return field.value
485+
? JSON.parse(field.value as string)
486+
: emptyEditorContent;
487+
} catch {
488+
return emptyEditorContent;
489+
}
490+
})()}
491+
onUpdate={(content) => {
492+
field.onChange(JSON.stringify(content));
493+
}}
494+
className="min-h-[200px]"
495+
/>
496+
</div>
476497
) : option.key === "defaultValue" ? (
477498
<Input
478499
{...field}
479-
type="text"
500+
type={selectedTypeName === "Link" ? "url" : "text"}
480501
onChange={field.onChange}
481502
value={(field.value ?? "") as string}
482503
/>

testplanit/app/[locale]/projects/repository/[projectId]/[caseId]/[version]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ export default function TestCaseVersions() {
407407
return (
408408
<div>
409409
{previousFieldValue !== undefined && previousFieldValue !== null && (
410-
<div className="relative p-1 rounded">
410+
<div className="relative p-1 rounded" data-testid="diff-removed">
411411
<div className="absolute inset-0 bg-red-500/20 rounded pointer-events-none" />
412412
<span className="relative text-red-600 dark:text-red-400 flex space-x-1 items-center">
413413
<div>
@@ -417,7 +417,7 @@ export default function TestCaseVersions() {
417417
</span>
418418
</div>
419419
)}
420-
<div className="relative p-1 rounded">
420+
<div className="relative p-1 rounded" data-testid="diff-added">
421421
<div className="absolute inset-0 bg-green-500/20 rounded pointer-events-none" />
422422
<span className="relative text-green-600 dark:text-green-400 flex space-x-1 items-center">
423423
<div>

testplanit/e2e/page-objects/repository/repository.page.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,52 @@ export class RepositoryPage extends BasePage {
134134
const folder = this.getFolderById(folderId);
135135
await expect(folder).toBeVisible({ timeout: 10000 });
136136

137-
// Use evaluate to click via JavaScript to ensure we're clicking the exact element
138-
await folder.evaluate((el) => {
139-
(el as HTMLElement).click();
140-
});
137+
// Close any open dropdown menus by pressing Escape
138+
await this.page.keyboard.press('Escape');
139+
await this.page.waitForTimeout(100);
140+
141+
// Use retry logic to ensure the folder is actually selected
142+
await expect(async () => {
143+
// Close any menus that may have opened
144+
await this.page.keyboard.press('Escape');
145+
146+
// Scroll folder into view
147+
await folder.scrollIntoViewIfNeeded();
148+
149+
// Find the folder icon specifically (svg element after the button)
150+
// The structure is: <button>(chevron)</button> <svg class="w-4 h-4">(icon)</svg>
151+
// Click on the icon which should trigger the folder click without opening menu
152+
const folderIcon = folder.locator('svg').nth(1); // Second svg is the folder icon
153+
const iconVisible = await folderIcon.isVisible().catch(() => false);
154+
155+
if (iconVisible) {
156+
await folderIcon.click();
157+
} else {
158+
// Fallback: click on the folder row but avoid the right side where menu is
159+
const box = await folder.boundingBox();
160+
if (box) {
161+
await this.page.mouse.click(box.x + 30, box.y + box.height / 2);
162+
} else {
163+
await folder.click();
164+
}
165+
}
166+
167+
// Wait for React to process the click
168+
await this.page.waitForTimeout(300);
169+
170+
// Verify the URL contains a node parameter
171+
const url = this.page.url();
172+
expect(url).toMatch(/node=\d+/);
173+
174+
// Verify this folder is now selected (has bg-secondary class indicating selection)
175+
const isSelected = await folder.evaluate((el) => {
176+
return el.classList.contains('bg-secondary') ||
177+
el.getAttribute('aria-selected') === 'true' ||
178+
el.closest('[aria-selected="true"]') !== null;
179+
});
180+
expect(isSelected).toBe(true);
181+
}).toPass({ timeout: 15000 });
141182

142-
// Wait for the URL to update with the selected folder
143-
await expect(this.page).toHaveURL(new RegExp(`node=${folderId}`), { timeout: 10000 });
144183
await this.page.waitForLoadState("networkidle");
145184
}
146185

testplanit/e2e/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default defineConfig({
3030

3131
// Limit workers for stability (dev server can get overwhelmed)
3232
// Production build can handle more workers than dev server
33-
workers: isCI ? 2 : useProdBuild ? 5 : 1,
33+
workers: isCI ? 2 : useProdBuild ? 2 : 1,
3434

3535
// Reporter configuration
3636
reporter: [

testplanit/e2e/tests/admin/users/user-updates.spec.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ test.describe("User Update Operations", () => {
252252
name: "Toggle Test User",
253253
email: testEmail,
254254
password: "password123",
255-
roleId: 1,
256255
access: "USER",
257256
});
258257

@@ -312,7 +311,6 @@ test.describe("User Update Operations", () => {
312311
name: "Edit Test User",
313312
email: testEmail,
314313
password: "password123",
315-
roleId: 1,
316314
access: "USER",
317315
});
318316

@@ -379,7 +377,6 @@ test.describe("User Update Operations", () => {
379377
name: "Delete Test User",
380378
email: testEmail,
381379
password: "password123",
382-
roleId: 1,
383380
access: "USER",
384381
});
385382

@@ -433,7 +430,6 @@ test.describe("User Update Operations", () => {
433430
name: "Restore Test User",
434431
email: testEmail,
435432
password: "password123",
436-
roleId: 1,
437433
access: "USER",
438434
});
439435

@@ -509,27 +505,26 @@ test.describe("User Update Operations", () => {
509505
await page.goto("/en-US");
510506
await page.waitForLoadState("networkidle");
511507

512-
// Open user menu (usually in top right)
513-
const userMenuButton = page
514-
.getByRole("button")
515-
.filter({ hasText: /AA|admin/i })
516-
.first();
508+
// Open user menu using stable test-id selector
509+
const userMenuButton = page.getByTestId("user-menu-trigger");
510+
await expect(userMenuButton).toBeVisible({ timeout: 5000 });
517511
await userMenuButton.click();
518512

519-
// Wait for dropdown menu
520-
await expect(page.getByRole("menu")).toBeVisible({ timeout: 3000 });
513+
// Wait for dropdown menu content
514+
const menuContent = page.getByTestId("user-menu-content");
515+
await expect(menuContent).toBeVisible({ timeout: 3000 });
521516

522-
// Look for theme submenu
523-
const themeMenu = page
524-
.getByRole("menuitem")
525-
.filter({ hasText: /theme|appearance/i })
526-
.first();
517+
// Open theme submenu using stable test-id
518+
const themeSubmenu = page.getByTestId("theme-submenu-trigger");
519+
if (await themeSubmenu.isVisible()) {
520+
await themeSubmenu.click();
527521

528-
if (await themeMenu.isVisible()) {
529-
await themeMenu.hover();
522+
// Wait for theme submenu content
523+
const themeContent = page.getByTestId("theme-submenu-content");
524+
await expect(themeContent).toBeVisible({ timeout: 3000 });
530525

531-
// Select a theme option
532-
const darkTheme = page
526+
// Select dark theme option
527+
const darkTheme = themeContent
533528
.getByRole("menuitem")
534529
.filter({ hasText: /dark/i })
535530
.first();
@@ -554,7 +549,6 @@ test.describe("User Update Operations", () => {
554549
name: "API Test User",
555550
email: testEmail,
556551
password: "password123",
557-
roleId: 1,
558552
access: "USER",
559553
});
560554

@@ -586,7 +580,6 @@ test.describe("User Update Operations", () => {
586580
name: "API Pref Test User",
587581
email: testEmail,
588582
password: "password123",
589-
roleId: 1,
590583
access: "USER",
591584
});
592585

@@ -620,7 +613,6 @@ test.describe("User Update Operations", () => {
620613
name: "Auth Test User",
621614
email: testEmail,
622615
password: "password123",
623-
roleId: 1,
624616
access: "USER",
625617
});
626618

testplanit/e2e/tests/reports/repository-stats-test-case-dimension.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ test.describe("Repository Statistics - Test Case Dimension", () => {
1010
async function getTestProjectId(
1111
api: import("../../fixtures/api.fixture").ApiHelper
1212
): Promise<number> {
13-
return await api.createProject(`E2E Report Test Project ${Date.now()}`);
13+
// Use timestamp + random suffix to ensure uniqueness across parallel test runs
14+
const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
15+
return await api.createProject(`E2E Report Project ${uniqueId}`);
1416
}
1517

1618
/**

testplanit/e2e/tests/repository/Test Repository Management/breadcrumbs.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ test.describe("Breadcrumbs", () => {
6565
await childTreeItem.click();
6666
await page.waitForLoadState("networkidle");
6767

68-
// Verify URL contains the child folder node
69-
await expect(page).toHaveURL(new RegExp(`node=${childId}`), { timeout: 5000 });
68+
// Verify URL contains a node parameter (folder selection)
69+
await expect(page).toHaveURL(/node=\d+/, { timeout: 5000 });
7070

7171
// Verify breadcrumbs show both parent and child (proving we're in child folder)
7272
const breadcrumbs = page.locator('nav[aria-label="breadcrumb"]');
@@ -83,8 +83,8 @@ test.describe("Breadcrumbs", () => {
8383
await parentBreadcrumb.click({ force: true });
8484
await page.waitForLoadState("networkidle");
8585

86-
// Verify we navigated to parent folder - URL should contain node=parentId
87-
await expect(page).toHaveURL(new RegExp(`node=${parentId}`), { timeout: 5000 });
86+
// Verify we navigated to parent folder - URL should contain a node parameter
87+
await expect(page).toHaveURL(/node=\d+/, { timeout: 5000 });
8888
});
8989

9090
test("Deep Nested Breadcrumbs", async ({ api, page }) => {

0 commit comments

Comments
 (0)