Skip to content

Commit 7e1a3c9

Browse files
authored
Merge pull request rancher-sandbox#8914 from tylercritchlow/container-logs-volumes-e2e-tests
Container logs and Volumes page e2e tests
2 parents f7a3281 + 42497ca commit 7e1a3c9

File tree

11 files changed

+689
-20
lines changed

11 files changed

+689
-20
lines changed

.github/actions/spelling/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
aarch
22
abbrv
3+
actionmenu
34
ACTIONSTART
45
activedirectory
56
addexclusion

e2e/container-logs.e2e.spec.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import {expect, test, ElectronApplication, Page} from '@playwright/test';
2+
3+
import {NavPage} from './pages/nav-page';
4+
import {ContainerLogsPage} from './pages/container-logs-page';
5+
import {startSlowerDesktop, teardown, tool} from './utils/TestUtils';
6+
7+
let page: Page;
8+
9+
test.describe.serial('Container Logs Tests', () => {
10+
let electronApp: ElectronApplication;
11+
let testContainerId: string;
12+
let testContainerName: string;
13+
14+
test.beforeAll(async ({}, testInfo) => {
15+
[electronApp, page] = await startSlowerDesktop(testInfo, {
16+
kubernetes: {enabled: false},
17+
containerEngine: {allowedImages: {enabled: false}}
18+
});
19+
20+
const navPage = new NavPage(page);
21+
await navPage.progressBecomesReady();
22+
});
23+
24+
test.afterAll(async ({}, testInfo) => {
25+
if (testContainerId) {
26+
try {
27+
await tool('docker', 'rm', '-f', testContainerId);
28+
} catch (error) {
29+
}
30+
}
31+
await teardown(electronApp, testInfo);
32+
});
33+
34+
test('should navigate to containers page', async () => {
35+
const navPage = new NavPage(page);
36+
const containersPage = await navPage.navigateTo('Containers');
37+
38+
await expect(navPage.mainTitle).toHaveText('Containers');
39+
await containersPage.waitForTableToLoad();
40+
});
41+
42+
test('should create and display test container', async () => {
43+
testContainerName = `test-logs-container-${Date.now()}`;
44+
45+
const output = await tool('docker', 'run', '--detach', '--name', testContainerName,
46+
'alpine', 'sh', '-c', 'echo "Starting"; for i in $(seq 1 10); do echo "L$i: msg$i"; done; echo "Finished"'); testContainerId = output.trim();
47+
48+
expect(testContainerId).toMatch(/^[a-f0-9]{64}$/);
49+
50+
await page.reload();
51+
52+
const navPage = new NavPage(page);
53+
const containersPage = await navPage.navigateTo('Containers');
54+
await containersPage.waitForTableToLoad();
55+
56+
await containersPage.waitForContainerToAppear(testContainerId);
57+
await containersPage.viewContainerLogs(testContainerId);
58+
59+
await page.waitForURL(`**/containers/logs/${testContainerId}`, {timeout: 10_000});
60+
});
61+
62+
test('should display container logs page', async () => {
63+
const containerLogsPage = new ContainerLogsPage(page);
64+
65+
await expect(containerLogsPage.containerInfo).toBeVisible();
66+
67+
await expect(containerLogsPage.terminal).toBeVisible();
68+
await expect(containerLogsPage.loadingIndicator).not.toBeVisible();
69+
});
70+
71+
test('should show container information', async () => {
72+
const containerLogsPage = new ContainerLogsPage(page);
73+
74+
await expect(containerLogsPage.containerInfo).toBeVisible();
75+
76+
await expect(containerLogsPage.containerName).toContainText(testContainerName);
77+
await expect(containerLogsPage.containerState).not.toBeEmpty();
78+
});
79+
80+
test('should display logs content', async () => {
81+
const containerLogsPage = new ContainerLogsPage(page);
82+
83+
await containerLogsPage.waitForLogsToLoad();
84+
85+
await expect(containerLogsPage.terminal).toContainText('L1: msg1');
86+
});
87+
88+
test('should support log search', async () => {
89+
const containerLogsPage = new ContainerLogsPage(page);
90+
91+
await expect(containerLogsPage.searchInput).toBeVisible();
92+
93+
const searchTerm = 'msg';
94+
await containerLogsPage.searchLogs(searchTerm);
95+
96+
const searchHighlight = page.locator('span.xterm-decoration-top');
97+
await expect(searchHighlight).toBeVisible();
98+
99+
const highlightedRow = containerLogsPage.terminal.locator('.xterm-rows div', {
100+
has: page.locator('.xterm-decoration-top')
101+
});
102+
103+
await expect(highlightedRow).toContainText('L1: msg1');
104+
105+
await containerLogsPage.searchNextButton.click();
106+
107+
await expect(searchHighlight).toBeVisible();
108+
await expect(highlightedRow).toContainText('L2: msg2');
109+
110+
await containerLogsPage.searchPrevButton.click();
111+
112+
await expect(searchHighlight).toBeVisible();
113+
await expect(highlightedRow).toContainText('L1: msg1');
114+
115+
await containerLogsPage.searchClearButton.click();
116+
await expect(containerLogsPage.searchInput).toBeEmpty();
117+
118+
await containerLogsPage.terminal.click();
119+
120+
await expect(searchHighlight).not.toBeVisible();
121+
});
122+
123+
test('should handle terminal scrolling', async () => {
124+
const scrollTestContainerName = `test-scroll-container-${Date.now()}`;
125+
let scrollTestContainerId: string;
126+
127+
try {
128+
const output = await tool('docker', 'run', '--detach', '--name', scrollTestContainerName,
129+
'alpine', 'sh', '-c', 'for i in $(seq 1 100); do echo "Line $i:"; done; sleep 1');
130+
scrollTestContainerId = output.trim();
131+
132+
const navPage = new NavPage(page);
133+
const containersPage = await navPage.navigateTo('Containers');
134+
135+
await page.reload();
136+
await containersPage.waitForTableToLoad();
137+
138+
await containersPage.waitForContainerToAppear(scrollTestContainerId);
139+
await containersPage.viewContainerLogs(scrollTestContainerId);
140+
141+
await page.waitForURL(`**/containers/logs/${scrollTestContainerId}`, {timeout: 10_000});
142+
143+
const containerLogsPage = new ContainerLogsPage(page);
144+
await containerLogsPage.waitForLogsToLoad();
145+
146+
const terminalRows = containerLogsPage.terminal.locator('.xterm-rows');
147+
const lastLine = terminalRows.getByText('Line 100:', { exact: false });
148+
const firstLine = terminalRows.getByText('Line 1:', { exact: false });
149+
150+
await expect(lastLine).toBeVisible();
151+
await expect(firstLine).not.toBeVisible();
152+
153+
await containerLogsPage.scrollToTop();
154+
155+
await expect(firstLine).toBeVisible();
156+
await expect(lastLine).not.toBeVisible();
157+
158+
await containerLogsPage.scrollToBottom();
159+
160+
await expect(lastLine).toBeVisible();
161+
await expect(firstLine).not.toBeVisible();
162+
} finally {
163+
if (scrollTestContainerId) {
164+
try {
165+
await tool('docker', 'rm', '-f', scrollTestContainerId);
166+
} catch (cleanupError) {
167+
}
168+
}
169+
}
170+
});
171+
172+
test('should output logs if container not exited', async () => {
173+
const longRunningContainerName = `test-not-exited-logs-${Date.now()}`;
174+
let longRunningContainerId: string;
175+
176+
try {
177+
const output = await tool('docker', 'run', '--detach', '--name', longRunningContainerName,
178+
'alpine', 'sh', '-c', 'while true; do echo "Log $(date +%s)"; sleep 2; done');
179+
longRunningContainerId = output.trim();
180+
181+
const navPage = new NavPage(page);
182+
const containersPage = await navPage.navigateTo('Containers');
183+
184+
await page.reload();
185+
await containersPage.waitForTableToLoad();
186+
187+
await containersPage.waitForContainerToAppear(longRunningContainerId);
188+
await containersPage.viewContainerLogs(longRunningContainerId);
189+
190+
await page.waitForURL(`**/containers/logs/${longRunningContainerId}`, {timeout: 10000});
191+
192+
const containerLogsPage = new ContainerLogsPage(page);
193+
await containerLogsPage.waitForLogsToLoad();
194+
195+
const locator = containerLogsPage.terminal.locator('.xterm-screen');
196+
await expect(locator.getByText(/Log \d+/).nth(1)).toBeVisible();
197+
198+
await expect(containerLogsPage.terminal).toContainText('Log ');
199+
200+
await tool('docker', 'rm', '-f', longRunningContainerId);
201+
} finally {
202+
if (longRunningContainerId) {
203+
try {
204+
await tool('docker', 'rm', '-f', longRunningContainerId);
205+
} catch (cleanupError) {
206+
}
207+
}
208+
}
209+
});
210+
});

e2e/pages/container-logs-page.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type {Locator, Page} from '@playwright/test';
2+
import {expect} from '@playwright/test';
3+
4+
export class ContainerLogsPage {
5+
readonly page: Page;
6+
readonly terminal: Locator;
7+
readonly containerInfo: Locator;
8+
readonly containerName: Locator;
9+
readonly containerState: Locator;
10+
readonly searchWidget: Locator;
11+
readonly searchInput: Locator;
12+
readonly searchPrevButton: Locator;
13+
readonly searchNextButton: Locator;
14+
readonly searchClearButton: Locator;
15+
readonly errorMessage: Locator;
16+
readonly loadingIndicator: Locator;
17+
18+
constructor(page: Page) {
19+
this.page = page;
20+
21+
this.terminal = page.getByTestId('terminal');
22+
23+
this.containerInfo = page.getByTestId('container-info');
24+
this.containerName = page.getByTestId('container-name');
25+
this.containerState = page.getByTestId('container-state');
26+
27+
this.searchWidget = page.getByTestId('search-widget');
28+
this.searchInput = page.getByTestId('search-input');
29+
this.searchPrevButton = page.getByTestId('search-prev-btn');
30+
this.searchNextButton = page.getByTestId('search-next-btn');
31+
this.searchClearButton = page.getByTestId('search-clear-btn');
32+
33+
this.loadingIndicator = page.getByTestId('loading-indicator');
34+
this.errorMessage = page.getByTestId('error-message');
35+
}
36+
37+
async waitForLogsToLoad() {
38+
await expect(this.terminal).toBeVisible();
39+
await expect(this.loadingIndicator).toBeHidden({ timeout: 30_000 });
40+
}
41+
42+
async searchLogs(searchTerm: string) {
43+
await this.searchInput.fill(searchTerm);
44+
await this.searchInput.press('Enter');
45+
}
46+
47+
async scrollToBottom() {
48+
await this.page.evaluate(() => {
49+
const viewport = document.querySelector('.xterm-viewport');
50+
if (viewport) {
51+
viewport.scrollTop = viewport.scrollHeight;
52+
}
53+
});
54+
}
55+
56+
async scrollToTop() {
57+
await this.page.evaluate(() => {
58+
const viewport = document.querySelector('.xterm-viewport');
59+
if (viewport) {
60+
viewport.scrollTop = 0;
61+
}
62+
});
63+
}
64+
65+
async getScrollPosition(): Promise<number> {
66+
return await this.page.evaluate(() => {
67+
const viewport = document.querySelector('.xterm-viewport');
68+
return viewport ? viewport.scrollTop : 0;
69+
});
70+
}
71+
}

e2e/pages/containers-page.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,67 @@
1-
import { Page } from '@playwright/test';
1+
import {Locator, Page, expect} from '@playwright/test';
2+
3+
type ActionString = 'logs' | 'stop' | 'start' | 'delete';
24

35
export class ContainersPage {
46
readonly page: Page;
7+
readonly table: Locator;
8+
readonly namespaceSelector: Locator;
59

610
constructor(page: Page) {
711
this.page = page;
12+
this.table = page.locator('.sortable-table');
13+
this.namespaceSelector = page.locator('.select-namespace');
14+
}
15+
16+
getContainerRow(containerId: string) {
17+
return this.table.locator(`tr.main-row[data-node-id="${containerId}"]`);
18+
}
19+
20+
async waitForContainerToAppear(containerId: string, timeout = 30_000) {
21+
const containerRow = this.getContainerRow(containerId);
22+
await expect(containerRow).toBeVisible({timeout});
23+
}
24+
25+
async clickContainerAction(containerId: string, action: ActionString) {
26+
const containerRow = this.getContainerRow(containerId);
27+
// The action button is in the actions column with class 'btn role-multi-action'
28+
await containerRow.locator('.btn.role-multi-action').click();
29+
30+
// Wait for the action menu to appear and click the action by text
31+
const actionText = {
32+
logs: 'Logs',
33+
stop: 'Stop',
34+
start: 'Start',
35+
delete: 'Delete',
36+
}[action];
37+
38+
const actionLocator = this.page.getByTestId("actionmenu").getByText(actionText, {exact: true});
39+
await actionLocator.click();
40+
}
41+
42+
async viewContainerLogs(containerId: string) {
43+
await this.clickContainerAction(containerId, 'logs');
844
}
45+
46+
async stopContainer(containerId: string) {
47+
await this.clickContainerAction(containerId, 'stop');
48+
}
49+
50+
async startContainer(containerId: string) {
51+
await this.clickContainerAction(containerId, 'start');
52+
}
53+
54+
async deleteContainer(containerId: string) {
55+
await this.clickContainerAction(containerId, 'delete');
56+
}
57+
58+
async getContainerCount(): Promise<number> {
59+
const rows = this.table.locator('tr.main-row');
60+
return await rows.count();
61+
}
62+
63+
async waitForTableToLoad() {
64+
await this.table.waitFor({state: 'visible'});
65+
}
66+
967
}

e2e/pages/nav-page.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import util from 'util';
22

3-
import { ContainersPage } from './containers-page';
4-
import { DiagnosticsPage } from './diagnostics-page';
5-
import { ExtensionsPage } from './extensions-page';
6-
import { ImagesPage } from './images-page';
7-
import { K8sPage } from './k8s-page';
8-
import { PortForwardPage } from './portforward-page';
9-
import { SnapshotsPage } from './snapshots-page';
10-
import { TroubleshootingPage } from './troubleshooting-page';
11-
import { WSLIntegrationsPage } from './wsl-integrations-page';
12-
import { tool } from '../utils/TestUtils';
13-
14-
import type { Page, Locator } from '@playwright/test';
3+
import {ContainersPage} from './containers-page';
4+
import {DiagnosticsPage} from './diagnostics-page';
5+
import {ExtensionsPage} from './extensions-page';
6+
import {ImagesPage} from './images-page';
7+
import {K8sPage} from './k8s-page';
8+
import {PortForwardPage} from './portforward-page';
9+
import {SnapshotsPage} from './snapshots-page';
10+
import {TroubleshootingPage} from './troubleshooting-page';
11+
import {VolumesPage} from './volumes-page';
12+
import {WSLIntegrationsPage} from './wsl-integrations-page';
13+
import {tool} from '../utils/TestUtils';
14+
15+
import type {Locator, Page} from '@playwright/test';
1516

1617
const pageConstructors = {
1718
General: (page: Page) => page,
@@ -24,6 +25,7 @@ const pageConstructors = {
2425
Snapshots: (page: Page) => new SnapshotsPage(page),
2526
Diagnostics: (page: Page) => new DiagnosticsPage(page),
2627
Extensions: (page: Page) => new ExtensionsPage(page),
28+
Volumes: (page: Page) => new VolumesPage(page),
2729
};
2830

2931
export class NavPage {

0 commit comments

Comments
 (0)