Skip to content

Commit af28b8d

Browse files
authored
test: add E2E tests for protos (#3244)
* feat: add E2E tests for protos resource (#3092) - Add Page Object Model for protos pages - Add list page tests with pagination - Add CRUD tests with required fields only - Add CRUD tests with all fields - All 11 tests passing Fixes #3092 * test: add UI verification to proto details page - Navigate to proto details via list page and View button - Verify proto content is displayed correctly in the UI - Addresses feedback from code review * test: use UI for update and delete operations in proto tests - Update protos via UI (Edit button -> modify content -> Save) - Delete protos via UI (Delete button -> confirm dialog) - Add UI verification to read/view tests - API now only used for initial setup verification and cleanup - All 8 tests passing Addresses feedback to use UI for all operations except setup/cleanup
1 parent e611715 commit af28b8d

File tree

4 files changed

+576
-0
lines changed

4 files changed

+576
-0
lines changed

e2e/pom/protos.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
import { uiGoto } from '@e2e/utils/ui';
18+
import { expect, type Page } from '@playwright/test';
19+
20+
const locator = {
21+
getProtoNavBtn: (page: Page) =>
22+
page.getByRole('link', { name: 'Protos' }),
23+
getAddProtoBtn: (page: Page) =>
24+
page.getByRole('button', { name: 'Add Proto' }),
25+
getAddBtn: (page: Page) =>
26+
page.getByRole('button', { name: 'Add', exact: true }),
27+
};
28+
29+
const assert = {
30+
isIndexPage: async (page: Page) => {
31+
await expect(page).toHaveURL((url) => url.pathname.endsWith('/protos'));
32+
const title = page.getByRole('heading', { name: 'Protos' });
33+
await expect(title).toBeVisible();
34+
},
35+
isAddPage: async (page: Page) => {
36+
await expect(page).toHaveURL((url) =>
37+
url.pathname.endsWith('/protos/add')
38+
);
39+
const title = page.getByRole('heading', { name: 'Add Proto' });
40+
await expect(title).toBeVisible();
41+
},
42+
isDetailPage: async (page: Page) => {
43+
await expect(page).toHaveURL((url) =>
44+
url.pathname.includes('/protos/detail')
45+
);
46+
const title = page.getByRole('heading', { name: 'Proto Detail' });
47+
await expect(title).toBeVisible();
48+
},
49+
};
50+
51+
const goto = {
52+
toIndex: (page: Page) => uiGoto(page, '/protos'),
53+
toAdd: (page: Page) => uiGoto(page, '/protos/add'),
54+
};
55+
56+
export const protosPom = {
57+
...locator,
58+
...assert,
59+
...goto,
60+
};
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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+
import { protosPom } from '@e2e/pom/protos';
19+
import { e2eReq } from '@e2e/utils/req';
20+
import { test } from '@e2e/utils/test';
21+
import { expect } from '@playwright/test';
22+
23+
import { API_PROTOS } from '@/config/constant';
24+
import type { APISIXType } from '@/types/schema/apisix';
25+
26+
const protoContent = `syntax = "proto3";
27+
package test;
28+
29+
message TestMessage {
30+
string name = 1;
31+
int32 age = 2;
32+
string email = 3;
33+
}`;
34+
35+
let createdProtoId: string;
36+
37+
test.describe('CRUD proto with all fields', () => {
38+
test.describe.configure({ mode: 'serial' });
39+
40+
test.afterAll(async () => {
41+
// cleanup: delete the proto
42+
if (createdProtoId) {
43+
await e2eReq.delete(`${API_PROTOS}/${createdProtoId}`).catch(() => {
44+
// ignore error if proto doesn't exist
45+
});
46+
}
47+
});
48+
49+
test('should create a proto with all fields', async ({ page }) => {
50+
await test.step('navigate to add proto page', async () => {
51+
await protosPom.toAdd(page);
52+
await protosPom.isAddPage(page);
53+
});
54+
55+
await test.step('fill in all fields', async () => {
56+
// Fill Content (ID is auto-generated, proto only has content field)
57+
await page.getByLabel('Content').fill(protoContent);
58+
});
59+
60+
await test.step('submit the form', async () => {
61+
await page.getByRole('button', { name: 'Add', exact: true }).click();
62+
63+
// Should redirect to list page after successful creation
64+
await protosPom.isIndexPage(page);
65+
});
66+
67+
await test.step('verify proto was created via API', async () => {
68+
// Get the list of protos to find the created one
69+
const protos = await e2eReq
70+
.get<unknown, APISIXType['RespProtoList']>(API_PROTOS)
71+
.then((v) => v.data);
72+
73+
// Find the proto with our content (search for exact package name)
74+
const createdProto = protos.list.find((p) =>
75+
p.value.content?.includes('package test;')
76+
);
77+
expect(createdProto).toBeDefined();
78+
expect(createdProto?.value.id).toBeDefined();
79+
// eslint-disable-next-line playwright/no-conditional-in-test
80+
createdProtoId = createdProto?.value.id || '';
81+
82+
// Verify content matches
83+
expect(createdProto?.value.content).toBe(protoContent);
84+
});
85+
});
86+
87+
test('should read/view the proto details', async ({ page }) => {
88+
await test.step('verify proto can be retrieved via API', async () => {
89+
const proto = await e2eReq
90+
.get<unknown, APISIXType['RespProtoDetail']>(
91+
`${API_PROTOS}/${createdProtoId}`
92+
)
93+
.then((v) => v.data);
94+
95+
expect(proto.value?.id).toBe(createdProtoId);
96+
expect(proto.value?.content).toBe(protoContent);
97+
expect(proto.value?.create_time).toBeDefined();
98+
expect(proto.value?.update_time).toBeDefined();
99+
});
100+
101+
await test.step('navigate to proto details page and verify UI', async () => {
102+
// Navigate to protos list page first
103+
await protosPom.toIndex(page);
104+
await protosPom.isIndexPage(page);
105+
106+
// Find and click the View button for the created proto
107+
const row = page.locator('tr').filter({ hasText: createdProtoId });
108+
await row.getByRole('button', { name: 'View' }).click();
109+
110+
// Verify we're on the detail page
111+
await protosPom.isDetailPage(page);
112+
113+
// Verify the content is displayed correctly on the details page
114+
const pageContent = await page.textContent('body');
115+
expect(pageContent).toContain('package test;');
116+
expect(pageContent).toContain('TestMessage');
117+
});
118+
});
119+
120+
test('should update the proto with new values', async ({ page }) => {
121+
const updatedContent = `syntax = "proto3";
122+
package test_updated;
123+
124+
message UpdatedTestMessage {
125+
string updated_name = 1;
126+
int32 updated_age = 2;
127+
string email = 3;
128+
bool is_active = 4;
129+
}`;
130+
131+
await test.step('navigate to proto detail page', async () => {
132+
// Should already be on detail page from previous test, but navigate to be safe
133+
await protosPom.toIndex(page);
134+
await protosPom.isIndexPage(page);
135+
136+
const row = page.locator('tr').filter({ hasText: createdProtoId });
137+
await row.getByRole('button', { name: 'View' }).click();
138+
await protosPom.isDetailPage(page);
139+
});
140+
141+
await test.step('enter edit mode and update content', async () => {
142+
// Click Edit button to enter edit mode
143+
await page.getByRole('button', { name: 'Edit' }).click();
144+
145+
// Clear and fill the content field
146+
const contentField = page.getByLabel('Content');
147+
await contentField.clear();
148+
await contentField.fill(updatedContent);
149+
});
150+
151+
await test.step('save the changes', async () => {
152+
// Click Save button
153+
await page.getByRole('button', { name: 'Save' }).click();
154+
155+
// Verify we're back in detail view mode
156+
await protosPom.isDetailPage(page);
157+
});
158+
159+
await test.step('verify proto was updated', async () => {
160+
// Verify the updated content is displayed
161+
const pageContent = await page.textContent('body');
162+
expect(pageContent).toContain('package test_updated');
163+
expect(pageContent).toContain('UpdatedTestMessage');
164+
165+
// Also verify via API
166+
const proto = await e2eReq
167+
.get<unknown, APISIXType['RespProtoDetail']>(
168+
`${API_PROTOS}/${createdProtoId}`
169+
)
170+
.then((v) => v.data);
171+
172+
expect(proto.value?.id).toBe(createdProtoId);
173+
expect(proto.value?.content).toBe(updatedContent);
174+
});
175+
});
176+
177+
test('should delete the proto', async ({ page }) => {
178+
await test.step('navigate to detail page and delete', async () => {
179+
// Navigate to protos list page first
180+
await protosPom.toIndex(page);
181+
await protosPom.isIndexPage(page);
182+
183+
// Find and click the View button
184+
const row = page.locator('tr').filter({ hasText: createdProtoId });
185+
await row.getByRole('button', { name: 'View' }).click();
186+
await protosPom.isDetailPage(page);
187+
188+
// Click Delete button
189+
await page.getByRole('button', { name: 'Delete' }).click();
190+
191+
// Confirm deletion in the dialog
192+
const deleteDialog = page.getByRole('dialog', { name: 'Delete Proto' });
193+
await expect(deleteDialog).toBeVisible();
194+
await deleteDialog.getByRole('button', { name: 'Delete' }).click();
195+
});
196+
197+
await test.step('verify deletion and redirect', async () => {
198+
// Should redirect to list page after deletion
199+
await protosPom.isIndexPage(page);
200+
201+
// Verify proto is not in the list (check in table cells specifically)
202+
await expect(page.getByRole('cell', { name: createdProtoId })).toBeHidden();
203+
});
204+
205+
await test.step('verify proto was deleted via API', async () => {
206+
await expect(async () => {
207+
await e2eReq.get(`${API_PROTOS}/${createdProtoId}`);
208+
}).rejects.toThrow();
209+
});
210+
});
211+
});

0 commit comments

Comments
 (0)