mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com> Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
This commit is contained in:
co-authored by
Shrinath-O2
ktx-akshay
parent
c1b9a1ca75
commit
c90a6fa949
@@ -23,7 +23,10 @@
|
||||
"alerts-composite-preview.spec.js",
|
||||
"alerts-composite-detail.spec.js",
|
||||
"alerts-evaluation-chart.spec.js",
|
||||
"alerts-chart-error-state.spec.js"
|
||||
"alerts-chart-error-state.spec.js",
|
||||
"alertsPagination.spec.js",
|
||||
"alertTemplatesPagination.spec.js",
|
||||
"alertDestinationsPagination.spec.js"
|
||||
],
|
||||
"disabled": [
|
||||
{
|
||||
@@ -100,7 +103,8 @@
|
||||
"crossLinkMultiStream.spec.js",
|
||||
"dashboards-form-validation.spec.js",
|
||||
"dashboard-refresh-without-cache.spec.js",
|
||||
"dashboard-print-layout.spec.js"
|
||||
"dashboard-print-layout.spec.js",
|
||||
"dashboardPagination.spec.js"
|
||||
],
|
||||
"disabled": [
|
||||
{
|
||||
|
||||
@@ -471,6 +471,50 @@ export class AlertDestinationsPage {
|
||||
await this.page.locator(this.addDestinationButton).waitFor({ state: 'visible', timeout: 30000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-link straight to the destinations list with a given `page` query param,
|
||||
* exercising AlertsDestinationList's URL-restored `currentPage` on a cold mount.
|
||||
* @param {number} page
|
||||
*/
|
||||
async gotoDestinationsWithPageParam(page) {
|
||||
const baseUrl = process.env.ZO_BASE_URL || 'http://localhost:5080';
|
||||
const orgIdentifier = process.env.ORGNAME || 'default';
|
||||
await this.page.goto(
|
||||
`${baseUrl}/web/alert-destinations?org_identifier=${orgIdentifier}&page=${page}`,
|
||||
{ waitUntil: 'domcontentloaded' }
|
||||
);
|
||||
await this.waitForDestinationListReady();
|
||||
}
|
||||
|
||||
// ==================== LIST PAGINATION (OTable pagination bar) ====================
|
||||
|
||||
async clickNextPage() {
|
||||
const nextPageBtn = this.page.locator('[data-test="o2-table-next-page-btn"]');
|
||||
await nextPageBtn.waitFor({ state: 'visible', timeout: 15000 });
|
||||
await nextPageBtn.click();
|
||||
}
|
||||
|
||||
async getPaginationInfoText() {
|
||||
const text = await this.page.locator('[data-test="o2-table-pagination-info"]').textContent().catch(() => null);
|
||||
return (text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
async expectPaginationInfoToMatch(pattern) {
|
||||
await expect
|
||||
.poll(async () => await this.getPaginationInfoText(), { timeout: 20000 })
|
||||
.toMatch(pattern);
|
||||
}
|
||||
|
||||
async expectUrlHasPageParam(pageValue) {
|
||||
await expect.poll(() => this.page.url(), { timeout: 15000 }).toContain(`page=${pageValue}`);
|
||||
}
|
||||
|
||||
async expectAtLeastOneListRow() {
|
||||
await expect(
|
||||
this.page.locator('[data-test^="o2-table-row-"]').first(),
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
/** @param {string} destinationName @param {string} url @param {string} templateName */
|
||||
async createDestination(destinationName, url, templateName) {
|
||||
await this.navigateToDestinations();
|
||||
|
||||
@@ -145,6 +145,50 @@ export class AlertTemplatesPage {
|
||||
await this.page.locator(this.addTemplateButton).waitFor({ state: 'visible', timeout: 30000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-link straight to the templates list with a given `page` query param,
|
||||
* exercising TemplateList's URL-restored `currentPage` on a cold mount.
|
||||
* @param {number} page
|
||||
*/
|
||||
async gotoTemplatesWithPageParam(page) {
|
||||
const baseUrl = process.env.ZO_BASE_URL || 'http://localhost:5080';
|
||||
const orgIdentifier = process.env.ORGNAME || 'default';
|
||||
await this.page.goto(
|
||||
`${baseUrl}/web/alert-templates?org_identifier=${orgIdentifier}&page=${page}`,
|
||||
{ waitUntil: 'domcontentloaded' }
|
||||
);
|
||||
await this.waitForTemplateListReady();
|
||||
}
|
||||
|
||||
// ==================== LIST PAGINATION (OTable pagination bar) ====================
|
||||
|
||||
async clickNextPage() {
|
||||
const nextPageBtn = this.page.locator('[data-test="o2-table-next-page-btn"]');
|
||||
await nextPageBtn.waitFor({ state: 'visible', timeout: 15000 });
|
||||
await nextPageBtn.click();
|
||||
}
|
||||
|
||||
async getPaginationInfoText() {
|
||||
const text = await this.page.locator('[data-test="o2-table-pagination-info"]').textContent().catch(() => null);
|
||||
return (text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
async expectPaginationInfoToMatch(pattern) {
|
||||
await expect
|
||||
.poll(async () => await this.getPaginationInfoText(), { timeout: 20000 })
|
||||
.toMatch(pattern);
|
||||
}
|
||||
|
||||
async expectUrlHasPageParam(pageValue) {
|
||||
await expect.poll(() => this.page.url(), { timeout: 15000 }).toContain(`page=${pageValue}`);
|
||||
}
|
||||
|
||||
async expectAtLeastOneListRow() {
|
||||
await expect(
|
||||
this.page.locator('[data-test^="o2-table-row-"]').first(),
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a template via API first, fall back to UI if API fails
|
||||
* @param {string} templateName - Name of the template
|
||||
|
||||
@@ -1673,6 +1673,37 @@ export class AlertsPage {
|
||||
await expect(this.page.locator('[data-test="o2-empty-state"]')).toBeVisible();
|
||||
}
|
||||
|
||||
// ==================== LIST PAGINATION (OTable pagination bar) ====================
|
||||
|
||||
// Click the list's next-page button. Disabled on the last page, so callers must
|
||||
// assert they are on a multi-page folder first.
|
||||
async clickNextPage() {
|
||||
const nextPageBtn = this.page.locator('[data-test="o2-table-next-page-btn"]');
|
||||
await nextPageBtn.waitFor({ state: 'visible', timeout: 15000 });
|
||||
await nextPageBtn.click();
|
||||
}
|
||||
|
||||
// Read the pagination info text ("Showing X - Y of Z"), whitespace-normalised.
|
||||
async getPaginationInfoText() {
|
||||
const text = await this.page.locator('[data-test="o2-table-pagination-info"]').textContent().catch(() => null);
|
||||
return (text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
// Assert the pagination info matches a regex (auto-retrying).
|
||||
async expectPaginationInfoToMatch(pattern) {
|
||||
await expect
|
||||
.poll(async () => await this.getPaginationInfoText(), { timeout: 20000 })
|
||||
.toMatch(pattern);
|
||||
}
|
||||
|
||||
// Assert the list rendered at least one data row (not the empty state), which
|
||||
// proves a page switch/clamp landed on a populated page rather than an empty flash.
|
||||
async expectAtLeastOneListRow() {
|
||||
await expect(
|
||||
this.page.locator('[data-test^="o2-table-row-"]').first(),
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
async verifyFolderExistsError() {
|
||||
await expect(this.page.getByText(this.locators.folderExistsError)).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -221,6 +221,262 @@ class APICleanup {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alerts folder via the v2 folders API.
|
||||
* @param {string} name - Unique folder name
|
||||
* @returns {Promise<{folderId: string, name: string}>}
|
||||
*/
|
||||
async createAlertFolder(name) {
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/v2/${this.org}/folders/alerts`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description: '' })
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`createAlertFolder: HTTP ${response.status} — ${body}`);
|
||||
}
|
||||
const folder = await response.json();
|
||||
testLogger.info('Created alert folder', { name, folderId: folder.folderId });
|
||||
return { folderId: folder.folderId, name: folder.name };
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `count` minimal scheduled alerts into a folder.
|
||||
* Alert creation validates the source stream exists, so a single row is ingested
|
||||
* into a dedicated stream first.
|
||||
* @param {string} folderId
|
||||
* @param {number} count
|
||||
* @param {string} prefix - Alert name prefix (deterministic ordering by name); also used
|
||||
* to derive the source stream name
|
||||
* @returns {Promise<Array<{alertId: string, name: string}>>}
|
||||
*/
|
||||
async seedAlertsInFolder(folderId, count, prefix) {
|
||||
const streamName = `${prefix}_stream`;
|
||||
await this._fetch(`${this.baseUrl}/api/${this.org}/${streamName}/_json`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify([{ message: 'e2e pagination seed row' }])
|
||||
});
|
||||
await this.waitForStreamSchema(streamName);
|
||||
|
||||
// Alert creation requires at least one destination (or workflow) attached.
|
||||
const templateName = `${prefix}_tmpl`;
|
||||
const destinationName = `${prefix}_dest`;
|
||||
await this.createAlertTemplate(templateName);
|
||||
await this.createAlertDestination(destinationName, templateName);
|
||||
|
||||
const created = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const name = `${prefix}_${i}`;
|
||||
const payload = {
|
||||
name,
|
||||
stream_type: 'logs',
|
||||
stream_name: streamName,
|
||||
is_real_time: false,
|
||||
query_condition: {
|
||||
type: 'custom',
|
||||
conditions: { version: 2, conditions: { filterType: 'group', logicalOperator: 'AND', conditions: [] } },
|
||||
sql: null, promql: null, promql_condition: null, aggregation: null,
|
||||
vrl_function: null, search_event_type: null, multi_time_range: [],
|
||||
},
|
||||
trigger_condition: {
|
||||
period: 10, operator: '>=', threshold: 3, frequency: 10, cron: '',
|
||||
frequency_type: 'minutes', silence: 10, timezone: 'UTC', align_time: true,
|
||||
},
|
||||
destinations: [destinationName], context_attributes: {}, row_template: '', enabled: false,
|
||||
};
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/v2/${this.org}/alerts?folder=${folderId}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`seedAlertsInFolder: HTTP ${response.status} — ${body}`);
|
||||
}
|
||||
created.push({ name });
|
||||
}
|
||||
return { alerts: created, templateName, destinationName, streamName };
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll a freshly-ingested stream's settings endpoint until its schema is
|
||||
* registered. Alert creation validates the source stream exists, and schema
|
||||
* registration can lag a moment behind the ingest write on a busy shared env.
|
||||
* @param {string} streamName
|
||||
* @param {number} maxWaitMs
|
||||
*/
|
||||
async waitForStreamSchema(streamName, maxWaitMs = 15000) {
|
||||
const deadline = Date.now() + maxWaitMs;
|
||||
while (Date.now() < deadline) {
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/${this.org}/streams/${streamName}/settings?type=logs`,
|
||||
{ method: 'GET', headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
if (response.ok) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
testLogger.warn('Stream schema did not register in time', { streamName, maxWaitMs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all alert templates (prebuilt + custom).
|
||||
* @returns {Promise<Array>} Array of template objects
|
||||
*/
|
||||
async fetchAlertTemplates() {
|
||||
try {
|
||||
const response = await this._fetch(`${this.baseUrl}/api/${this.org}/alerts/templates`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
testLogger.error('Failed to fetch alert templates', { status: response.status });
|
||||
return [];
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to fetch alert templates', { error: error.message });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alert notification template via API.
|
||||
* @param {string} name
|
||||
* @param {string} [body] - Defaults to a minimal valid template body
|
||||
*/
|
||||
async createAlertTemplate(name, body = null) {
|
||||
const templateBody = body || '{"text": "{alert_name} is active"}';
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/${this.org}/alerts/templates`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, body: templateBody })
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errBody = await response.text();
|
||||
throw new Error(`createAlertTemplate: HTTP ${response.status} — ${errBody}`);
|
||||
}
|
||||
testLogger.info('Created alert template via API', { name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `count` minimal alert templates.
|
||||
* @param {number} count
|
||||
* @param {string} prefix
|
||||
* @returns {Promise<Array<{name: string}>>}
|
||||
*/
|
||||
async seedAlertTemplates(count, prefix) {
|
||||
const created = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const name = `${prefix}_${i}`;
|
||||
await this.createAlertTemplate(name);
|
||||
created.push({ name });
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single alert template by name.
|
||||
* @param {string} name
|
||||
* @returns {Promise<Object>} { code, message }
|
||||
*/
|
||||
async deleteAlertTemplate(name) {
|
||||
try {
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/${this.org}/alerts/templates/${encodeURIComponent(name)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
const text = await response.text();
|
||||
return { code: response.ok ? 200 : response.status, message: text };
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to delete alert template', { name, error: error.message });
|
||||
return { code: 500, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP alert destination via API. example.com is IANA-reserved and never
|
||||
* contacted — no spec fires these alerts, so the destination just needs a URL the
|
||||
* server's SSRF guard accepts (it blocks localhost/private targets).
|
||||
* @param {string} name
|
||||
* @param {string} templateName - Must reference an existing template
|
||||
*/
|
||||
async createAlertDestination(name, templateName) {
|
||||
const payload = {
|
||||
name,
|
||||
url: 'http://example.com/e2e_pag_sink',
|
||||
method: 'post',
|
||||
template: templateName,
|
||||
type: 'http',
|
||||
headers: {}
|
||||
};
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/${this.org}/alerts/destinations`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errBody = await response.text();
|
||||
throw new Error(`createAlertDestination: HTTP ${response.status} — ${errBody}`);
|
||||
}
|
||||
testLogger.info('Created alert destination via API', { name });
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `count` minimal HTTP alert destinations, all sharing one template.
|
||||
* @param {number} count
|
||||
* @param {string} prefix
|
||||
* @param {string} templateName
|
||||
* @returns {Promise<Array<{name: string}>>}
|
||||
*/
|
||||
async seedAlertDestinations(count, prefix, templateName) {
|
||||
const created = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const name = `${prefix}_${i}`;
|
||||
await this.createAlertDestination(name, templateName);
|
||||
created.push({ name });
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single alert destination by name.
|
||||
* @param {string} name
|
||||
* @returns {Promise<Object>} { code, message }
|
||||
*/
|
||||
async deleteAlertDestination(name) {
|
||||
try {
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/${this.org}/alerts/destinations/${encodeURIComponent(name)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
const text = await response.text();
|
||||
return { code: response.ok ? 200 : response.status, message: text };
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to delete alert destination', { name, error: error.message });
|
||||
return { code: 500, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all dashboard folders
|
||||
* @returns {Promise<Array>} Array of folder objects
|
||||
@@ -248,6 +504,73 @@ class APICleanup {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dashboards folder via the v2 folders API.
|
||||
* @param {string} name - Unique folder name
|
||||
* @returns {Promise<{folderId: string, name: string}>}
|
||||
*/
|
||||
async createDashboardFolder(name) {
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/v2/${this.org}/folders/dashboards`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description: '', icon: null })
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`createDashboardFolder: HTTP ${response.status} — ${body}`);
|
||||
}
|
||||
const folder = await response.json();
|
||||
testLogger.info('Created dashboard folder', { name, folderId: folder.folderId });
|
||||
return { folderId: folder.folderId, name: folder.name };
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `count` minimal dashboards into a folder via createMinimalDashboard.
|
||||
* @param {string} folderId
|
||||
* @param {number} count
|
||||
* @param {string} prefix - Dashboard title prefix (deterministic ordering by name)
|
||||
* @returns {Promise<Array<{dashboardId: string, title: string}>>}
|
||||
*/
|
||||
async seedDashboardsInFolder(folderId, count, prefix) {
|
||||
const created = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const title = `${prefix}_${i}`;
|
||||
const { dashboardId } = await this.createMinimalDashboard(title, folderId);
|
||||
created.push({ dashboardId, title });
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a dashboards folder by id. Dashboards inside must be removed first,
|
||||
* otherwise the backend returns 400 ("Folder contains dashboards").
|
||||
* @param {string} folderId
|
||||
* @returns {Promise<Object>} { code, message }
|
||||
*/
|
||||
async deleteDashboardFolder(folderId) {
|
||||
try {
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/v2/${this.org}/folders/dashboards/${folderId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
const text = await response.text();
|
||||
if (response.ok) {
|
||||
return { code: 200, message: text };
|
||||
}
|
||||
testLogger.warn('Failed to delete dashboard folder', { folderId, status: response.status, body: text });
|
||||
return { code: response.status, message: text };
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to delete dashboard folder', { folderId, error: error.message });
|
||||
return { code: 500, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all dashboards in a specific folder
|
||||
* @param {string} folderName - The folder name (e.g., 'default')
|
||||
@@ -302,7 +625,9 @@ class APICleanup {
|
||||
throw new Error(`createMinimalDashboard: HTTP ${response.status} — ${body}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
const dashboardId = result.dashboard_id || result.dashboardId || result.id;
|
||||
// MetaDashboard nests the actual data under the version key (e.g. "v8").
|
||||
const inner = result[`v${result.version}`] || result;
|
||||
const dashboardId = inner.dashboardId || inner.dashboard_id || result.dashboard_id || result.id;
|
||||
testLogger.info('Created minimal dashboard', { dashboardId, folderId });
|
||||
return { dashboardId, folderId };
|
||||
}
|
||||
|
||||
@@ -69,8 +69,25 @@ export default class DashboardFolder {
|
||||
await expect(this.getFolderTitleByName(folderName)).toHaveCount(0);
|
||||
}
|
||||
|
||||
// The rail's ONavGroup flyout is anchored over the folder sidebar and only
|
||||
// closes on mouseleave, so park the pointer clear of it before clicking a
|
||||
// folder — otherwise the flyout overlay intercepts the click.
|
||||
async dismissNavFlyout() {
|
||||
const flyout = this.page
|
||||
.locator('[data-test^="nav-group-flyout-"]')
|
||||
.first();
|
||||
if (!(await flyout.isVisible().catch(() => false))) return;
|
||||
const viewport = this.page.viewportSize();
|
||||
await this.page.mouse.move(
|
||||
Math.floor((viewport?.width ?? 1280) * 0.75),
|
||||
Math.floor((viewport?.height ?? 720) / 2)
|
||||
);
|
||||
await flyout.waitFor({ state: "hidden", timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
// Click on a folder row by name to open it
|
||||
async openFolderByName(folderName) {
|
||||
await this.dismissNavFlyout();
|
||||
const folderCard = this.getFolderCardByName(folderName);
|
||||
await folderCard.waitFor({ state: "visible", timeout: 10000 });
|
||||
await folderCard.click();
|
||||
|
||||
@@ -7,6 +7,10 @@ const { gotoWithRetry } = require("../../playwright-tests/utils/navigation.js");
|
||||
export default class DashboardListPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
// List-level pagination controls (OTable pagination bar) and the pagination
|
||||
// info span that renders "Showing X - Y of Z".
|
||||
this.nextPageBtn = page.locator('[data-test="o2-table-next-page-btn"]');
|
||||
this.paginationInfo = page.locator('[data-test="o2-table-pagination-info"]');
|
||||
}
|
||||
|
||||
// Duplicate dashboard
|
||||
@@ -132,4 +136,61 @@ export default class DashboardListPage {
|
||||
await nameCell.waitFor({ state: "visible", timeout: 15000 });
|
||||
await nameCell.click();
|
||||
}
|
||||
|
||||
// Click the list's next-page button (OTable pagination bar). Disabled on the
|
||||
// last page, so callers assert they are on a multi-page folder first.
|
||||
async clickNextPage() {
|
||||
await this.nextPageBtn.waitFor({ state: "visible", timeout: 15000 });
|
||||
await this.nextPageBtn.click();
|
||||
}
|
||||
|
||||
// Read the pagination info text ("Showing X - Y of Z"), whitespace-normalised.
|
||||
// The span interpolates across template newlines, so collapse runs of
|
||||
// whitespace to single spaces before returning.
|
||||
async getPaginationInfoText() {
|
||||
const text = await this.paginationInfo.textContent().catch(() => null);
|
||||
return (text || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// Assert the pagination info matches a regex (auto-retrying). The span renders
|
||||
// "Showing {from} - {to} of {count}", e.g. "Showing 21 - 25 of 25".
|
||||
async expectPaginationInfoToMatch(pattern) {
|
||||
await expect
|
||||
.poll(async () => await this.getPaginationInfoText(), { timeout: 20000 })
|
||||
.toMatch(pattern);
|
||||
}
|
||||
|
||||
// Assert the current URL carries `page=<pageValue>`.
|
||||
async expectUrlHasPageParam(pageValue) {
|
||||
await expect.poll(() => this.page.url(), { timeout: 15000 }).toContain(`page=${pageValue}`);
|
||||
}
|
||||
|
||||
// Assert the current URL no longer carries `page=<pageValue>`.
|
||||
async expectUrlNotHasPageParam(pageValue) {
|
||||
await expect.poll(() => this.page.url(), { timeout: 15000 }).not.toContain(`page=${pageValue}`);
|
||||
}
|
||||
|
||||
// Assert the current URL carries no `page=` query param at all.
|
||||
async expectUrlHasNoPageParam() {
|
||||
await expect.poll(() => this.page.url(), { timeout: 15000 }).not.toMatch(/[?&]page=\d+/);
|
||||
}
|
||||
|
||||
// Click the first visible dashboard name cell in the list (used to open a
|
||||
// dashboard from the current page without searching, which would change the
|
||||
// pagination state).
|
||||
async clickFirstDashboardNameCell() {
|
||||
const firstCell = this.page
|
||||
.locator('[data-test^="dashboard-name-cell-"]')
|
||||
.first();
|
||||
await firstCell.waitFor({ state: "visible", timeout: 15000 });
|
||||
await firstCell.click();
|
||||
}
|
||||
|
||||
// Assert the list rendered at least one data row (not the empty state), which
|
||||
// proves a page switch landed on a populated page rather than an empty flash.
|
||||
async expectAtLeastOneRow() {
|
||||
await expect(
|
||||
this.page.locator('[data-test^="o2-table-row-"]').first(),
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
navigateToBase,
|
||||
} = require("../utils/enhanced-baseFixtures.js");
|
||||
const testLogger = require("../utils/test-logger.js");
|
||||
const PageManager = require("../../pages/page-manager.js");
|
||||
|
||||
// Page size for the Destinations list is 20 (AlertsDestinationList.vue :page-size="20").
|
||||
// Like TemplateList, AlertsDestinationList restores `currentPage` from the URL `page`
|
||||
// query param on mount, so a stale deep link reproduces the bug directly. Covers the
|
||||
// same OTable restorePage fix (#14745) as Dashboards/dashboardPagination.spec.js and
|
||||
// Alerts/alertsPagination.spec.js.
|
||||
const SINGLE_PAGE_COUNT = 2;
|
||||
const MULTI_PAGE_COUNT = 25;
|
||||
|
||||
const uniqueSeed = () =>
|
||||
`e2e_dest_pag_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
test.describe("Alert Destinations Pagination Reset testcases", () => {
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
let pm;
|
||||
let destinationNames = [];
|
||||
let templateName;
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
destinationNames = [];
|
||||
|
||||
const seed = uniqueSeed();
|
||||
templateName = `${seed}_tmpl`;
|
||||
await pm.apiCleanup.createAlertTemplate(templateName);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
for (const name of destinationNames) {
|
||||
await pm.apiCleanup.deleteAlertDestination(name);
|
||||
}
|
||||
await pm.apiCleanup.deleteAlertTemplate(templateName);
|
||||
});
|
||||
|
||||
test(
|
||||
"should clamp a stale deep-linked page to page 1 on a single-page destination list",
|
||||
{ tag: ["@destinations-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
const seed = uniqueSeed();
|
||||
const seeded = await pm.apiCleanup.seedAlertDestinations(
|
||||
SINGLE_PAGE_COUNT,
|
||||
seed,
|
||||
templateName,
|
||||
);
|
||||
destinationNames = seeded.map((d) => d.name);
|
||||
|
||||
// Compute a page guaranteed to be past the end, rather than a hardcoded number:
|
||||
// other specs/parallel tests share this org's destination list, so a fixed page
|
||||
// could accidentally land on a real page.
|
||||
const currentTotal = (await pm.apiCleanup.fetchDestinationsWithTemplateMapping()).destinations.length;
|
||||
const stalePage = Math.ceil(currentTotal / 20) + 10;
|
||||
|
||||
testLogger.info("Deep-linking to destinations with a stale page", { stalePage });
|
||||
await pm.alertDestinationsPage.gotoDestinationsWithPageParam(stalePage);
|
||||
|
||||
await pm.alertDestinationsPage.expectPaginationInfoToMatch(
|
||||
new RegExp(`Showing 1 - \\d+ of \\d+`),
|
||||
);
|
||||
await pm.alertDestinationsPage.expectAtLeastOneListRow();
|
||||
await pm.alertDestinationsPage.expectUrlHasPageParam("1");
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should navigate to page 2 and show the remaining destinations",
|
||||
{ tag: ["@destinations-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
const seed = uniqueSeed();
|
||||
const seeded = await pm.apiCleanup.seedAlertDestinations(
|
||||
MULTI_PAGE_COUNT,
|
||||
seed,
|
||||
templateName,
|
||||
);
|
||||
destinationNames = seeded.map((d) => d.name);
|
||||
|
||||
testLogger.info("Navigating to destinations and going to page 2");
|
||||
await pm.alertDestinationsPage.navigateToDestinations();
|
||||
await pm.alertDestinationsPage.waitForDestinationListReady();
|
||||
|
||||
await pm.alertDestinationsPage.clickNextPage();
|
||||
await pm.alertDestinationsPage.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
await pm.alertDestinationsPage.expectUrlHasPageParam("2");
|
||||
await pm.alertDestinationsPage.expectAtLeastOneListRow();
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
navigateToBase,
|
||||
} = require("../utils/enhanced-baseFixtures.js");
|
||||
const testLogger = require("../utils/test-logger.js");
|
||||
const PageManager = require("../../pages/page-manager.js");
|
||||
|
||||
// Page size for the Templates list is 20 (TemplateList.vue :page-size="20"). Unlike
|
||||
// Dashboards/AlertList, TemplateList restores `currentPage` from the URL `page` query
|
||||
// param on mount, so a stale deep link reproduces the bug directly with no folder
|
||||
// switch needed. Covers the same OTable restorePage fix (#14745) as
|
||||
// Dashboards/dashboardPagination.spec.js and Alerts/alertsPagination.spec.js.
|
||||
const SINGLE_PAGE_COUNT = 2;
|
||||
const MULTI_PAGE_COUNT = 25;
|
||||
|
||||
const uniqueSeed = () =>
|
||||
`e2e_tmpl_pag_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
test.describe("Alert Templates Pagination Reset testcases", () => {
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
let pm;
|
||||
let templateNames = [];
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
templateNames = [];
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
for (const name of templateNames) {
|
||||
await pm.apiCleanup.deleteAlertTemplate(name);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
"should clamp a stale deep-linked page to page 1 on a single-page template list",
|
||||
{ tag: ["@templates-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
const seed = uniqueSeed();
|
||||
const seeded = await pm.apiCleanup.seedAlertTemplates(SINGLE_PAGE_COUNT, seed);
|
||||
templateNames = seeded.map((t) => t.name);
|
||||
|
||||
// Compute a page guaranteed to be past the end, rather than a hardcoded number:
|
||||
// other specs/parallel tests share this org's template list, so a fixed page
|
||||
// could accidentally land on a real page.
|
||||
const currentTotal = (await pm.apiCleanup.fetchAlertTemplates()).length;
|
||||
const stalePage = Math.ceil(currentTotal / 20) + 10;
|
||||
|
||||
testLogger.info("Deep-linking to templates with a stale page", { stalePage });
|
||||
await pm.alertTemplatesPage.gotoTemplatesWithPageParam(stalePage);
|
||||
|
||||
await pm.alertTemplatesPage.expectPaginationInfoToMatch(
|
||||
new RegExp(`Showing 1 - \\d+ of \\d+`),
|
||||
);
|
||||
await pm.alertTemplatesPage.expectAtLeastOneListRow();
|
||||
await pm.alertTemplatesPage.expectUrlHasPageParam("1");
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should navigate to page 2 and show the remaining templates",
|
||||
{ tag: ["@templates-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
const seed = uniqueSeed();
|
||||
const seeded = await pm.apiCleanup.seedAlertTemplates(MULTI_PAGE_COUNT, seed);
|
||||
templateNames = seeded.map((t) => t.name);
|
||||
|
||||
testLogger.info("Navigating to templates and going to page 2");
|
||||
await pm.alertTemplatesPage.navigateToTemplates();
|
||||
await pm.alertTemplatesPage.waitForTemplateListReady();
|
||||
|
||||
await pm.alertTemplatesPage.clickNextPage();
|
||||
await pm.alertTemplatesPage.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
await pm.alertTemplatesPage.expectUrlHasPageParam("2");
|
||||
await pm.alertTemplatesPage.expectAtLeastOneListRow();
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
navigateToBase,
|
||||
} = require("../utils/enhanced-baseFixtures.js");
|
||||
const testLogger = require("../utils/test-logger.js");
|
||||
const PageManager = require("../../pages/page-manager.js");
|
||||
|
||||
// Page size for the Alerts list is 20. Folder A has more than one page and
|
||||
// Folder B has a single page so "reset to page 1" and "clamp a stale page" are
|
||||
// both observable and unambiguous. Mirrors Dashboards/dashboardPagination.spec.js,
|
||||
// which covers the same OTable restorePage fix (#14745) for the Dashboards list.
|
||||
const FOLDER_A_COUNT = 25;
|
||||
const FOLDER_B_COUNT = 2;
|
||||
|
||||
// Unique per-test seed prefix. Tests run in parallel against a shared org, so
|
||||
// every test seeds its own folders/alerts/streams and tears them down in afterEach.
|
||||
const uniqueSeed = () =>
|
||||
`e2e_alert_pag_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
test.describe("Alerts Folder Switch Pagination Reset testcases", () => {
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
let pm;
|
||||
let folderA;
|
||||
let folderB;
|
||||
let streamA;
|
||||
let streamB;
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
|
||||
// Seed per test: each test is fully independent under parallel workers.
|
||||
const seed = uniqueSeed();
|
||||
const prefixA = `${seed}_a`;
|
||||
const prefixB = `${seed}_b`;
|
||||
streamA = `${prefixA}_stream`;
|
||||
streamB = `${prefixB}_stream`;
|
||||
|
||||
folderA = await pm.apiCleanup.createAlertFolder(`${seed}_A`);
|
||||
folderB = await pm.apiCleanup.createAlertFolder(`${seed}_B`);
|
||||
await pm.apiCleanup.seedAlertsInFolder(folderA.folderId, FOLDER_A_COUNT, prefixA);
|
||||
await pm.apiCleanup.seedAlertsInFolder(folderB.folderId, FOLDER_B_COUNT, prefixB);
|
||||
|
||||
await pm.alertsPage.navigateToAlertsPage();
|
||||
testLogger.info("Test setup completed", {
|
||||
folderA: folderA.name,
|
||||
folderB: folderB.name,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// Delete seeded alerts first: the folder delete is rejected while it still
|
||||
// contains alerts.
|
||||
const alertsA = await pm.apiCleanup.fetchAlertsInFolder(folderA.folderId);
|
||||
for (const alert of alertsA) {
|
||||
await pm.apiCleanup.deleteAlert(alert.alert_id, folderA.folderId);
|
||||
}
|
||||
const alertsB = await pm.apiCleanup.fetchAlertsInFolder(folderB.folderId);
|
||||
for (const alert of alertsB) {
|
||||
await pm.apiCleanup.deleteAlert(alert.alert_id, folderB.folderId);
|
||||
}
|
||||
await pm.apiCleanup.deleteFolder(folderA.folderId);
|
||||
await pm.apiCleanup.deleteFolder(folderB.folderId);
|
||||
await pm.apiCleanup.deleteStream(streamA);
|
||||
await pm.apiCleanup.deleteStream(streamB);
|
||||
});
|
||||
|
||||
test(
|
||||
"should reset the list to page 1 when switching folders",
|
||||
{ tag: ["@alerts-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
testLogger.info("Opening folder A and navigating to page 2");
|
||||
await pm.alertsPage.navigateToFolder(folderA.name);
|
||||
await pm.alertsPage.expectPaginationInfoToMatch(/Showing 1 - 20 of 25/);
|
||||
|
||||
await pm.alertsPage.clickNextPage();
|
||||
await pm.alertsPage.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
|
||||
testLogger.info("Switching to folder B");
|
||||
await pm.alertsPage.navigateToFolder(folderB.name);
|
||||
await pm.alertsPage.expectPaginationInfoToMatch(/Showing 1 - 2 of 2/);
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should clamp to page 1 and render rows when switching to a shorter single-page folder",
|
||||
{ tag: ["@alerts-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
testLogger.info("Opening folder A and navigating to page 2");
|
||||
await pm.alertsPage.navigateToFolder(folderA.name);
|
||||
await pm.alertsPage.expectPaginationInfoToMatch(/Showing 1 - 20 of 25/);
|
||||
|
||||
await pm.alertsPage.clickNextPage();
|
||||
await pm.alertsPage.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
|
||||
testLogger.info("Switching to the shorter folder B");
|
||||
await pm.alertsPage.navigateToFolder(folderB.name);
|
||||
|
||||
await pm.alertsPage.expectPaginationInfoToMatch(/Showing 1 - 2 of 2/);
|
||||
await pm.alertsPage.expectAtLeastOneListRow();
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
navigateToBase,
|
||||
} = require("../utils/enhanced-baseFixtures.js");
|
||||
const testLogger = require("../utils/test-logger.js");
|
||||
const PageManager = require("../../pages/page-manager.js");
|
||||
const { waitForDashboardPage } = require("./utils/dashCreation.js");
|
||||
|
||||
// Page size for the Dashboards list is 20. Folder A has more than one page and
|
||||
// Folder B has a single page so "reset to page 1" and "clamp a stale page" are
|
||||
// both observable and unambiguous.
|
||||
const FOLDER_A_COUNT = 25;
|
||||
const FOLDER_B_COUNT = 2;
|
||||
|
||||
// Unique per-test seed prefix. Tests run in parallel against a shared org, so
|
||||
// every test seeds its own folders/dashboards and tears them down in afterEach.
|
||||
const uniqueSeed = () =>
|
||||
`e2e_pag_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
test.describe("Folder Switch Pagination Reset testcases", () => {
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
let pm;
|
||||
let folderA;
|
||||
let folderB;
|
||||
let dashboardsA = [];
|
||||
let dashboardsB = [];
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
|
||||
// Seed per test: each test is fully independent under parallel workers.
|
||||
const seed = uniqueSeed();
|
||||
folderA = await pm.apiCleanup.createDashboardFolder(`${seed}_A`);
|
||||
folderB = await pm.apiCleanup.createDashboardFolder(`${seed}_B`);
|
||||
dashboardsA = await pm.apiCleanup.seedDashboardsInFolder(
|
||||
folderA.folderId,
|
||||
FOLDER_A_COUNT,
|
||||
`${seed}_a`,
|
||||
);
|
||||
dashboardsB = await pm.apiCleanup.seedDashboardsInFolder(
|
||||
folderB.folderId,
|
||||
FOLDER_B_COUNT,
|
||||
`${seed}_b`,
|
||||
);
|
||||
|
||||
await pm.dashboardList.menuItem("dashboards-item");
|
||||
await waitForDashboardPage(page);
|
||||
testLogger.info("Test setup completed", {
|
||||
folderA: folderA.name,
|
||||
folderB: folderB.name,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// Delete seeded dashboards first: the folder delete is rejected while it
|
||||
// still contains dashboards.
|
||||
for (const dash of dashboardsA) {
|
||||
await pm.apiCleanup.deleteDashboard(dash.dashboardId, folderA.folderId);
|
||||
}
|
||||
for (const dash of dashboardsB) {
|
||||
await pm.apiCleanup.deleteDashboard(dash.dashboardId, folderB.folderId);
|
||||
}
|
||||
await pm.apiCleanup.deleteDashboardFolder(folderA.folderId);
|
||||
await pm.apiCleanup.deleteDashboardFolder(folderB.folderId);
|
||||
});
|
||||
|
||||
test(
|
||||
"should reset the list to page 1 and strip the page query param when switching folders",
|
||||
{ tag: ["@folder-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
testLogger.info("Opening folder A and navigating to page 2");
|
||||
await pm.dashboardFolder.openFolderByName(folderA.name);
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 1 - 20 of 25/);
|
||||
|
||||
await pm.dashboardList.clickNextPage();
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
await pm.dashboardList.expectUrlHasPageParam("2");
|
||||
|
||||
testLogger.info("Switching to folder B");
|
||||
await pm.dashboardFolder.openFolderByName(folderB.name);
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 1 - 2 of 2/);
|
||||
await pm.dashboardList.expectUrlHasNoPageParam();
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should preserve the current page when returning from a dashboard in the same folder",
|
||||
{ tag: ["@folder-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
testLogger.info("Opening folder A and navigating to page 2");
|
||||
await pm.dashboardFolder.openFolderByName(folderA.name);
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 1 - 20 of 25/);
|
||||
|
||||
await pm.dashboardList.clickNextPage();
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
await pm.dashboardList.expectUrlHasPageParam("2");
|
||||
|
||||
testLogger.info("Opening a dashboard from page 2");
|
||||
await pm.dashboardList.clickFirstDashboardNameCell();
|
||||
await page.waitForURL(/\/dashboards\/view/, { timeout: 30000 });
|
||||
|
||||
testLogger.info("Returning to the list");
|
||||
await pm.dashboardCreate.backToDashboardList();
|
||||
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
await pm.dashboardList.expectUrlHasPageParam("2");
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should clamp a stale deep-linked page to page 1 on a single-page folder",
|
||||
{ tag: ["@folder-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
testLogger.info("Deep-linking to folder B with a stale page=5");
|
||||
const orgId = process.env["ORGNAME"];
|
||||
await page.goto(
|
||||
`${process.env["ZO_BASE_URL"]}/web/dashboards?org_identifier=${orgId}&folder=${folderB.folderId}&page=5`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
await waitForDashboardPage(page);
|
||||
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 1 - 2 of 2/);
|
||||
await pm.dashboardList.expectUrlNotHasPageParam("5");
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should clamp to page 1 and render rows when switching to a shorter single-page folder",
|
||||
{ tag: ["@folder-pagination-reset", "@all"] },
|
||||
async ({ page }) => {
|
||||
testLogger.info("Opening folder A and navigating to page 2");
|
||||
await pm.dashboardFolder.openFolderByName(folderA.name);
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 1 - 20 of 25/);
|
||||
|
||||
await pm.dashboardList.clickNextPage();
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 21 - /);
|
||||
|
||||
testLogger.info("Switching to the shorter folder B");
|
||||
await pm.dashboardFolder.openFolderByName(folderB.name);
|
||||
|
||||
await pm.dashboardList.expectPaginationInfoToMatch(/Showing 1 - 2 of 2/);
|
||||
await pm.dashboardList.expectAtLeastOneRow();
|
||||
await pm.dashboardList.expectUrlHasNoPageParam();
|
||||
testLogger.info("Test completed");
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user