Skip to content

Commit c9ca14e

Browse files
authored
[FEATURE] Migrate UI to shadcn (#21)
- Better accessibility - Dark theme - Response header editor
1 parent bd1efc5 commit c9ca14e

Some content is hidden

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

56 files changed

+4686
-1492
lines changed

ui/components.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "default",
4+
"rsc": false,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "tailwind.config.js",
8+
"css": "src/index.css",
9+
"baseColor": "slate",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"aliases": {
14+
"components": "@/components",
15+
"utils": "@/lib/utils"
16+
}
17+
}
Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,90 @@
1-
import { expect } from '@playwright/test';
2-
31
export default {
4-
meta: expect.objectContaining({
2+
meta: {
53
schemaVersion: 'v5',
64
hoverflyVersion: 'v1.6.0',
75
timeExported: '2023-04-10T12:00:00Z'
8-
}),
9-
data: expect.objectContaining({
6+
},
7+
data: {
108
pairs: [
11-
expect.objectContaining({
12-
request: expect.objectContaining({
13-
method: [expect.objectContaining({ matcher: 'glob', value: 'GET' })],
14-
scheme: [expect.objectContaining({ matcher: 'glob', value: 'http' })],
15-
destination: [expect.objectContaining({ matcher: 'glob', value: 'mock.api.com' })],
9+
{
10+
request: {
11+
method: [
12+
{
13+
matcher: 'glob',
14+
value: 'GET'
15+
}
16+
],
17+
scheme: [
18+
{
19+
matcher: 'glob',
20+
value: 'http'
21+
}
22+
],
23+
destination: [
24+
{
25+
matcher: 'glob',
26+
value: 'mock.api.com'
27+
}
28+
],
1629
path: [
17-
expect.objectContaining({ matcher: 'glob', value: 'path1' }),
18-
expect.objectContaining({ matcher: 'exact', value: 'path2' })
30+
{
31+
matcher: 'glob',
32+
value: 'path1'
33+
},
34+
{
35+
matcher: 'exact',
36+
value: 'path2'
37+
}
1938
],
20-
query: expect.objectContaining({
21-
param1: [expect.objectContaining({ matcher: 'glob', value: 'value1' })],
39+
query: {
2240
param2: [
23-
expect.objectContaining({ matcher: 'exact', value: 'value2' }),
24-
expect.objectContaining({ matcher: 'exact', value: 'value3' })
41+
{
42+
matcher: 'exact',
43+
value: 'value2'
44+
},
45+
{
46+
matcher: 'exact',
47+
value: 'value3'
48+
}
49+
],
50+
param1: [
51+
{
52+
matcher: 'glob',
53+
value: 'value1'
54+
}
2555
]
26-
}),
27-
headers: expect.objectContaining({
56+
},
57+
headers: {
2858
header1: [
29-
expect.objectContaining({
59+
{
3060
matcher: 'exact',
3161
value: 'valueheader1',
32-
config: expect.objectContaining({
62+
config: {
3363
ignoreUnknown: true,
3464
ignoreOrder: true,
3565
ignoreOccurrences: true
36-
})
37-
})
66+
}
67+
}
3868
]
39-
}),
69+
},
4070
body: [
41-
expect.objectContaining({ matcher: 'jsonPartial', value: '{ "field1": "value1" }' })
71+
{
72+
matcher: 'jsonPartial',
73+
value: '{ "field1": "value1" }'
74+
}
4275
]
43-
}),
44-
response: expect.objectContaining({
76+
},
77+
response: {
4578
status: 204,
46-
body: '{\n "response": "body"\n}'
47-
})
48-
})
79+
body: '{\n "response": "body"\n}',
80+
encodedBody: true,
81+
fixedDelay: 500,
82+
headers: {
83+
'Content-Type': ['application/json'],
84+
'Cache-control': ['max-age=604800', 'must-revalidate']
85+
}
86+
}
87+
}
4988
]
50-
})
89+
}
5190
};

ui/e2e/utils/WebUiSimulationPage.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ export class WebUiSimulationPage {
3737
}
3838

3939
await this.page.goto('/');
40-
41-
await expect(this.page.getByText('loading')).not.toBeVisible({ timeout: 30_000 });
40+
await expect(this.page.getByText('Simulations')).toBeVisible();
4241
}
4342

4443
/**
@@ -63,12 +62,13 @@ export class WebUiSimulationPage {
6362
return await this.page.evaluate<string>('navigator.clipboard.readText()');
6463
}
6564

66-
getSelectMatcherType(nth = 0) {
67-
return this.page.getByTestId('select-matcher').nth(nth);
65+
async selectMatcherOption(tab: Locator, optionLabel: string) {
66+
await tab.getByLabel('Matcher').click();
67+
await this.page.getByText(optionLabel, { exact: true }).click();
6868
}
6969

7070
getMatcherInput(nth = 0) {
71-
return this.page.getByTestId('matcher-input-value').nth(nth);
71+
return this.page.getByLabel('Value').nth(nth);
7272
}
7373

7474
getAddMatcherButton(nth = 0) {

ui/e2e/web-ui.spec.ts

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ test('should create a full simulation', async ({ page }) => {
3838
await page.getByRole('tab', { name: 'Method' }).click();
3939
const currentTab = simulationPage.requestTabContentMethod;
4040
await currentTab.getByRole('button', { name: 'Add first field matcher for method' }).click();
41-
await currentTab.locator(simulationPage.getSelectMatcherType()).selectOption('glob');
41+
await simulationPage.selectMatcherOption(currentTab, 'Glob');
4242
await currentTab.locator(simulationPage.getMatcherInput()).fill('GET');
4343
});
4444

4545
await test.step('Edit request HTTP Scheme', async () => {
4646
await page.getByRole('tab', { name: 'Scheme' }).click();
4747
const currentTab = simulationPage.requestTabContentScheme;
4848
await currentTab.getByRole('button', { name: 'Add first field matcher for scheme' }).click();
49-
await currentTab.locator(simulationPage.getSelectMatcherType()).selectOption('glob');
49+
await simulationPage.selectMatcherOption(currentTab, 'Glob');
5050
await currentTab.locator(simulationPage.getMatcherInput()).fill('http');
5151
});
5252

@@ -56,15 +56,15 @@ test('should create a full simulation', async ({ page }) => {
5656
await currentTab
5757
.getByRole('button', { name: 'Add first field matcher for destination' })
5858
.click();
59-
await currentTab.locator(simulationPage.getSelectMatcherType()).selectOption('glob');
59+
await simulationPage.selectMatcherOption(currentTab, 'Glob');
6060
await currentTab.locator(simulationPage.getMatcherInput()).fill('mock.api.com');
6161
});
6262

6363
await test.step('Edit request HTTP Path', async () => {
6464
await page.getByRole('tab', { name: 'Path' }).click();
6565
const currentTab = simulationPage.requestTabContentPath;
66-
await currentTab.getByRole('button', { name: 'Add first field matcher for method' }).click();
67-
await currentTab.locator(simulationPage.getSelectMatcherType()).selectOption('glob');
66+
await currentTab.getByRole('button', { name: 'Add first field matcher for path' }).click();
67+
await simulationPage.selectMatcherOption(currentTab, 'Glob');
6868
await currentTab.locator(simulationPage.getMatcherInput()).fill('path1');
6969

7070
await currentTab.locator(simulationPage.getAddMatcherButton()).click();
@@ -81,7 +81,7 @@ test('should create a full simulation', async ({ page }) => {
8181
await currentTab
8282
.locator(page.getByRole('button', { name: "Add first field matcher for query 'param1'" }))
8383
.click();
84-
await currentTab.locator(simulationPage.getSelectMatcherType()).selectOption('glob');
84+
await simulationPage.selectMatcherOption(currentTab, 'Glob');
8585
await currentTab.locator(simulationPage.getMatcherInput()).fill('value1');
8686

8787
await currentTab
@@ -91,10 +91,10 @@ test('should create a full simulation', async ({ page }) => {
9191
await currentTab
9292
.locator(page.getByRole('button', { name: "Add first field matcher for query 'param2'" }))
9393
.click();
94-
await currentTab.locator(simulationPage.getMatcherInput(1)).fill('value2');
94+
await currentTab.locator(simulationPage.getMatcherInput()).fill('value2');
9595

96-
await currentTab.locator(simulationPage.getAddMatcherButton(1)).click();
97-
await currentTab.locator(simulationPage.getMatcherInput(2)).fill('value3');
96+
await currentTab.locator(simulationPage.getAddMatcherButton()).click();
97+
await currentTab.locator(simulationPage.getMatcherInput(1)).fill('value3');
9898
});
9999

100100
await test.step('Edit request HTTP Headers', async () => {
@@ -109,16 +109,17 @@ test('should create a full simulation', async ({ page }) => {
109109
.click();
110110

111111
await currentTab.locator(simulationPage.getMatcherInput()).fill('valueheader1');
112-
await currentTab.locator(page.getByText('Ignore Unknown')).click();
113-
await currentTab.locator(page.getByText('Ignore Order')).click();
114-
await currentTab.locator(page.getByText('Ignore Occurrences')).click();
112+
await currentTab.getByLabel('Advanced options').click();
113+
await page.getByText('Ignore Unknown').click();
114+
await page.getByText('Ignore Order').click();
115+
await page.getByText('Ignore Occurrences').click();
115116
});
116117

117118
await test.step('Edit request HTTP Body', async () => {
118119
await page.getByRole('tab', { name: 'Body' }).click();
119120
const currentTab = simulationPage.requestTabContentBody;
120121
await currentTab.getByRole('button', { name: 'Add first field matcher for body' }).click();
121-
await currentTab.locator(simulationPage.getSelectMatcherType()).selectOption('jsonPartial');
122+
await simulationPage.selectMatcherOption(currentTab, 'JSON Partial');
122123
await currentTab.locator(simulationPage.getMatcherInput()).fill('{ "field1": "value1" }');
123124
});
124125

@@ -127,8 +128,44 @@ test('should create a full simulation', async ({ page }) => {
127128
simulationPage.responseBodyEditor,
128129
'{ "response": "body" }'
129130
);
131+
130132
await page.getByText('Prettify').click();
131-
await page.getByTestId('response-status-select').selectOption('204');
133+
await page.getByLabel('Status').click();
134+
await page.getByText('204').click();
135+
136+
await page.getByText('Encoded body').click();
137+
await page.getByRole('button', { name: 'Delay' }).click();
138+
await page.getByLabel('Fixed Delay (ms)').fill('500');
139+
});
140+
141+
await test.step('Edit response HTTP headers', async () => {
142+
// Delete a header
143+
await page.getByRole('button', { name: 'Add header' }).click();
144+
await page.getByLabel('Header name').fill('a header to be');
145+
await page.getByLabel('Header value').fill('removed');
146+
await page.getByText('Submit').click();
147+
await page.getByLabel('Delete response header').click();
148+
149+
// Add header (fill with keyboard)
150+
await page.getByRole('button', { name: 'Add header' }).click();
151+
await page.getByLabel('Header name').fill('Content-type');
152+
await page.getByLabel('Header name').press('Tab');
153+
await page.getByLabel('Header value').fill('application/json');
154+
await page.getByLabel('Header value').press('Enter');
155+
await expect(page.getByText('Content-type: application/json')).toBeVisible();
156+
157+
// Edit header
158+
await page.getByText('Content-type: application/json').click();
159+
await page.getByLabel('Header name').last().fill('Content-Type');
160+
await page.getByText('Submit').last().click();
161+
await expect(page.getByText('Content-Type: application/json')).toBeVisible();
162+
163+
// Add another header (with multiple values)
164+
await page.getByRole('button', { name: 'Add header' }).click();
165+
await page.getByLabel('Header name').last().fill('Cache-control');
166+
await page.getByLabel('Header value').last().fill('max-age=604800&must-revalidate');
167+
await page.getByText('Submit').last().click();
168+
await expect(page.getByText('Cache-control: max-age=604800,must-revalidate')).toBeVisible();
132169
});
133170

134171
await test.step('Assert text editor content is correct', async () => {
@@ -146,7 +183,8 @@ test('content-type headers should update when body change', async ({ page }) =>
146183
const simulationPage = new WebUiSimulationPage(page);
147184
await simulationPage.goto(JSON.stringify(simulationWithContentType));
148185

149-
await page.locator('.card-header').click();
186+
await page.getByRole('button', { name: '- →️ 200' }).click();
187+
150188
await simulationPage.setTextEditorContent(
151189
simulationPage.responseBodyEditor,
152190
', how is it going ?'

ui/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<link href="/favicon.ico" rel="icon"/>
66
<meta content="width=device-width, initial-scale=1" name="viewport"/>
77
<meta content="Hoverfly API UI Editor" name="description"/>
8-
8+
<link href="https://rsms.me/inter/inter.css" rel="stylesheet"/>
99
<title>Hoverfly Simulation UI</title>
1010
</head>
1111
<body>

0 commit comments

Comments
 (0)