feat(client): add tabs support (#52)

* feat(client): add `tabs` support

* chore: apply fixes

* test: fix types

* build: pw again

* build: more attempts to improve PW

* fix: regressions after refactoring to tabs

* style: apply TS style fixes

* build: more PW

* test: run a problematic test in serial mode

* test: no parallel pw

* test: fix race condition in PW

* test: pw one worker no retry
This commit is contained in:
Mazen Touati
2026-01-31 02:10:41 +01:00
committed by GitHub
parent bc43530a9e
commit 8c028773d8
40 changed files with 2135 additions and 877 deletions

5
tests/E2E/launch.sh Normal file → Executable file
View File

@@ -95,12 +95,12 @@ trap cleanup EXIT
cd "$TARGET_DIR"
echo "Starting main PHP server on port $PORT1..."
php artisan serve --host=127.0.0.1 --port="$PORT1" > php_server1.log 2>&1 &
PHP_CLI_SERVER_WORKERS=8 php artisan serve --host=127.0.0.1 --port="$PORT1" > php_server1.log 2>&1 &
PID1=$!
echo "Main PHP server PID: $PID1"
echo "Starting secondary PHP server on port $PORT2..."
php artisan serve --host=127.0.0.1 --port="$PORT2" > php_server2.log 2>&1 &
PHP_CLI_SERVER_WORKERS=8 php artisan serve --host=127.0.0.1 --port="$PORT2" > php_server2.log 2>&1 &
PID2=$!
echo "Secondary PHP server PID: $PID2"
@@ -135,6 +135,7 @@ append_env_var() {
append_env_var "APP_URL" "$BACKEND_URL"
append_env_var "NIMBUS_RELAY_ENDPOINT" "$SECONDARY_URL"
append_env_var "CACHE_STORE" "array"
echo "Servers are running..."

View File

@@ -14,11 +14,11 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: "./tests",
timeout: 160_000,
fullyParallel: false,
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
retries: 1,
workers: process.env.CI ? 4 : 8,
retries: 0,
workers: process.env.CI ? 1 : 8,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
@@ -33,15 +33,22 @@ export default defineConfig({
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
projects: !process.env.CI
? [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
]
: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});

View File

@@ -114,7 +114,7 @@ test("Dump and Die visualization sanity checklist", async ({ page }) => {
.click();
await expect(
page.locator("#reka-collapsible-content-v-130"),
page.locator("#reka-collapsible-content-v-146"),
).toMatchAriaSnapshot(`- text: "0: \\"/\\" (1) 1: \\"\\\\\\" (1)"`);
// dump #3 (runtime object)

View File

@@ -1,5 +1,7 @@
import { test, expect } from '../core/fixtures';
test.describe.configure({ mode: 'serial' });
test('Link Sharing complete workflow', async ({ page, basePage }) => {
// Arrange
@@ -71,13 +73,17 @@ test('Link Sharing complete workflow', async ({ page, basePage }) => {
await page.getByRole('button', { name: 'GET /show-logged-in-user' }).click();
await basePage.executeRequest();
// Verify that clearing storage resets the state
await page.context().clearCookies();
await page.evaluate(() => {
// Prevent any further writes to localStorage from the running app (race condition fix)
Storage.prototype.setItem = () => { };
localStorage.clear();
sessionStorage.clear();
});
await page.reload();
await basePage.goto();
// Assert - Verify state is empty

View File

@@ -0,0 +1,66 @@
import { test, expect } from '../core/fixtures';
test('Multiple tabs support and isolated responses', async ({ page, basePage }) => {
// Arrange
await basePage.goto();
// Act - Open first tab (Route 1)
await page.getByRole('button', { name: 'verbs' }).click();
await page.getByRole('button', { name: 'GET /verbs' }).click();
// Act - Execute request in first tab
await basePage.executeRequest();
await expect(page.getByTestId('response-status-badge')).toContainText('200');
// Assert - Verify it appears in Sidebar Tabs
const tabs = page.getByTestId('sidebar-tab');
await expect(tabs.filter({ hasText: /GET\s*verbs\/verbs/ })).toBeVisible();
// Act - Open second tab (Route 2)
await page.getByRole('button', { name: 'PATCH /verbs' }).click();
// Assert - Verify second tab is created and response is empty
await expect(tabs.filter({ hasText: /PATCH\s*verbs\/verbs/ })).toBeVisible();
await expect(page.getByTestId('response-empty')).toBeVisible();
// Act - Execute request in second tab
await basePage.executeRequest();
await expect(page.getByTestId('response-status-badge')).toContainText('422');
// Act - Switch back to first tab via Open Tabs menu
await tabs.filter({ hasText: /GET\s*verbs\/verbs/ }).click();
// Assert - Verify isolated response (should be 200 from Route 1)
await expect(page.getByTestId('response-status-badge')).toContainText('200');
// Act - Switch back to second tab via Open Tabs menu
await tabs.filter({ hasText: /PATCH\s*verbs\/verbs/ }).click();
// Assert - Verify isolated response (should be 422 from Route 2)
await expect(page.getByTestId('response-status-badge')).toContainText('422');
// Act - Close a tab
// Verify all tabs exist before closing
await expect(tabs).toHaveCount(2);
// Hover over the tab to show the close button and click it
const getTab = tabs.filter({ hasText: /GET\s*verbs\/verbs/ });
await getTab.hover();
await getTab.getByTestId('sidebar-tab-close').click();
// Assert - Verify tab is closed and only the other one remains
await expect(tabs).toHaveCount(1);
await expect(tabs.filter({ hasText: /GET\s*verbs\/verbs/ })).not.toBeVisible();
await expect(tabs.filter({ hasText: /PATCH\s*verbs\/verbs/ })).toBeVisible();
});