Skip to content

Commit c8d3466

Browse files
authored
test(e2e): regression suite + form-state refactor (fixes #3293, #3342, #3344, #3370) (#3391)
1 parent 697be33 commit c8d3466

70 files changed

Lines changed: 4242 additions & 323 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

e2e/server/apisix_conf.yml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,28 @@
1616
#
1717

1818
apisix:
19-
node_listen: 9080 # APISIX listening port
19+
node_listen: 9080
2020
enable_ipv6: false
2121
proxy_mode: http&stream
2222
stream_proxy:
2323
tcp:
2424
- 9100
2525
udp:
2626
- 9200
27-
2827
deployment:
2928
admin:
30-
allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow
31-
- 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test.
32-
29+
allow_admin:
30+
- 0.0.0.0/0
3331
admin_key:
34-
- name: "admin"
32+
- name: admin
3533
key: edd1c9f034335f136f87ad84b625c8f1
36-
role: admin # admin: manage all configuration data
37-
34+
role: admin
3835
etcd:
39-
host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
40-
- "http://etcd:2379" # multiple etcd address
41-
prefix: "/apisix" # apisix configurations prefix
42-
timeout: 30 # 30 seconds
43-
36+
host:
37+
- http://etcd:2379
38+
prefix: /apisix
39+
timeout: 30
4440
discovery:
4541
nacos:
4642
host:
47-
- "http://nacos:8848"
43+
- http://nacos:8848
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// Bulk D-01: routes list page must render correctly with 100 rows.
19+
// Data is seeded directly via the Admin API to keep the bulk-render
20+
// assertion decoupled from the add-form behavior.
21+
22+
import { routesPom } from '@e2e/pom/routes';
23+
import {
24+
bulkCreateRoutes,
25+
bulkDeleteRoutesByPrefix,
26+
} from '@e2e/utils/bulk';
27+
import { test } from '@e2e/utils/test';
28+
import { expect } from '@playwright/test';
29+
30+
const PREFIX = 'bulk100';
31+
const COUNT = 100;
32+
33+
test.describe.configure({ mode: 'serial', timeout: 120_000 });
34+
35+
test.beforeAll(async () => {
36+
await bulkDeleteRoutesByPrefix(PREFIX);
37+
await bulkCreateRoutes({ count: COUNT, prefix: PREFIX });
38+
});
39+
40+
test.afterAll(async () => {
41+
await bulkDeleteRoutesByPrefix(PREFIX);
42+
});
43+
44+
test('routes list shows correct total and renders default page', async ({
45+
page,
46+
}) => {
47+
const t0 = Date.now();
48+
await routesPom.toIndex(page);
49+
await routesPom.isIndexPage(page);
50+
51+
// Routes are listed in server-defined order (not insertion order), so
52+
// bulk100-0 isn't guaranteed to be on page 1. Just require *some*
53+
// seeded row to render.
54+
const someSeededRow = page
55+
.getByRole('row', { name: new RegExp(`${PREFIX}-\\d+`) })
56+
.first();
57+
await expect(someSeededRow).toBeVisible({ timeout: 5000 });
58+
const renderMs = Date.now() - t0;
59+
test
60+
.info()
61+
.annotations.push({ type: 'perf', description: `LCP-ish: ${renderMs}ms` });
62+
expect(renderMs, 'list page should render in < 5s with 100 rows').toBeLessThan(
63+
5000
64+
);
65+
});
66+
67+
test('pagination shows ≥ 10 pages at default page_size=10', async ({
68+
page,
69+
}) => {
70+
await routesPom.toIndex(page);
71+
await routesPom.isIndexPage(page);
72+
73+
// page=10 must be reachable (100 rows / 10 per page = 10 pages).
74+
const page10 = page.getByRole('listitem', { name: '10' });
75+
await expect(page10).toBeVisible();
76+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
/* eslint-disable playwright/no-wait-for-timeout -- regression test stabilization */
18+
19+
// Bulk D-03: with 1000 routes the list page must:
20+
// - render the first page within a reasonable budget
21+
// - allow URL-driven jump to a far page (e.g. ?page=50)
22+
// - not flood the Admin API with per-row requests
23+
//
24+
// Marked as `bulk` — exclude from default CI runs.
25+
26+
import { routesPom } from '@e2e/pom/routes';
27+
import {
28+
bulkCreateRoutes,
29+
bulkDeleteRoutesByPrefix,
30+
} from '@e2e/utils/bulk';
31+
import { test } from '@e2e/utils/test';
32+
import { expect } from '@playwright/test';
33+
34+
const PREFIX = 'bulk1k';
35+
const COUNT = 1000;
36+
37+
test.describe.configure({ mode: 'serial', timeout: 300_000 });
38+
39+
test.beforeAll(async () => {
40+
await bulkDeleteRoutesByPrefix(PREFIX);
41+
await bulkCreateRoutes({ count: COUNT, prefix: PREFIX });
42+
});
43+
44+
test.afterAll(async () => {
45+
await bulkDeleteRoutesByPrefix(PREFIX);
46+
});
47+
48+
test('first page renders within 5s with 1000 rows in etcd', async ({
49+
page,
50+
}) => {
51+
const t0 = Date.now();
52+
await routesPom.toIndex(page);
53+
await routesPom.isIndexPage(page);
54+
55+
await expect(
56+
page
57+
.getByRole('row', { name: new RegExp(`${PREFIX}-\\d+`) })
58+
.first()
59+
).toBeVisible({ timeout: 8000 });
60+
const ms = Date.now() - t0;
61+
test
62+
.info()
63+
.annotations.push({ type: 'perf', description: `first-page render: ${ms}ms` });
64+
expect(ms).toBeLessThan(8000);
65+
});
66+
67+
test('admin API call volume on list page load is bounded (≤ 5 GETs)', async ({
68+
page,
69+
}) => {
70+
const adminCalls: string[] = [];
71+
page.on('request', (req) => {
72+
if (req.method() === 'GET' && req.url().includes('/apisix/admin/routes')) {
73+
adminCalls.push(req.url());
74+
}
75+
});
76+
77+
await routesPom.toIndex(page);
78+
await routesPom.isIndexPage(page);
79+
await expect(
80+
page
81+
.getByRole('row', { name: new RegExp(`${PREFIX}-\\d+`) })
82+
.first()
83+
).toBeVisible({ timeout: 8000 });
84+
// Settle for any deferred queries.
85+
await page.waitForTimeout(800);
86+
87+
test.info().annotations.push({
88+
type: 'perf',
89+
description: `admin /routes GETs: ${adminCalls.length}`,
90+
});
91+
expect(
92+
adminCalls.length,
93+
'list page should not issue per-row GETs (≤ 5 total)'
94+
).toBeLessThanOrEqual(5);
95+
});

e2e/tests/consumer_groups.crud-required-fields.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,13 @@ test('should CRUD Consumer Group with required fields', async ({ page }) => {
117117
const idField = page.getByRole('textbox', { name: 'ID', exact: true });
118118
await expect(idField).toBeDisabled();
119119

120-
// Cancel without making changes
120+
// Cancel without making changes. The Edit→Cancel guard now always
121+
// confirms before discarding (see src/hooks/useEditCancelGuard.tsx).
121122
await page.getByRole('button', { name: 'Cancel' }).click();
123+
await page
124+
.getByRole('dialog')
125+
.getByRole('button', { name: 'Discard Changes' })
126+
.click();
122127

123128
// Verify we're back in detail view
124129
await consumerGroupsPom.isDetailPage(page);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
/* eslint-disable playwright/no-wait-for-timeout -- regression test stabilization */
18+
19+
// Edge: typing syntactically-invalid JSON in the plugin config editor must
20+
// either prevent submission or surface a visible error — never round-trip a
21+
// broken config to the Admin API.
22+
23+
import { routesPom } from '@e2e/pom/routes';
24+
import { e2eReq } from '@e2e/utils/req';
25+
import { test } from '@e2e/utils/test';
26+
import {
27+
uiFillMonacoEditor,
28+
uiGetMonacoEditor,
29+
} from '@e2e/utils/ui';
30+
import { expect } from '@playwright/test';
31+
32+
import { deleteAllRoutes } from '@/apis/routes';
33+
34+
const pluginName = 'cors';
35+
const invalidJson = '{ "allow_origins": "*", '; // missing closing brace + trailing comma
36+
37+
test.beforeAll(async () => {
38+
await deleteAllRoutes(e2eReq);
39+
});
40+
41+
test.afterAll(async () => {
42+
await deleteAllRoutes(e2eReq);
43+
});
44+
45+
test('invalid JSON in plugin editor cannot be saved', async ({ page }) => {
46+
await routesPom.toAdd(page);
47+
await routesPom.isAddPage(page);
48+
49+
await page.getByRole('button', { name: 'Select Plugins' }).click();
50+
const selectDialog = page.getByRole('dialog', { name: 'Select Plugins' });
51+
await selectDialog.getByPlaceholder('Search').fill(pluginName);
52+
await selectDialog
53+
.getByTestId(`plugin-${pluginName}`)
54+
.getByRole('button', { name: 'Add' })
55+
.click();
56+
57+
const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' });
58+
const editor = await uiGetMonacoEditor(page, addPluginDialog, false);
59+
await uiFillMonacoEditor(page, editor, invalidJson);
60+
61+
await addPluginDialog.getByRole('button', { name: 'Add' }).click();
62+
63+
// The dialog must remain open OR an error must surface — what we forbid
64+
// is silently accepting invalid JSON and closing the dialog as if it
65+
// were valid.
66+
await page.waitForTimeout(1000);
67+
const dialogStillOpen = await addPluginDialog
68+
.isVisible()
69+
.catch(() => false);
70+
const errorVisible = await page
71+
.getByText(/(invalid|json|parse|syntax)/i)
72+
.first()
73+
.isVisible()
74+
.catch(() => false);
75+
76+
expect(
77+
dialogStillOpen || errorVisible,
78+
'invalid JSON must NOT silently close the editor as if accepted'
79+
).toBe(true);
80+
});

0 commit comments

Comments
 (0)