mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com>
347 lines
13 KiB
JavaScript
347 lines
13 KiB
JavaScript
import { expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Page object for the shared DateTime picker (web/src/components/DateTime.vue),
|
|
* focused on the Copy / Paste controls added in PR #13374.
|
|
*
|
|
* The same component backs Logs, Traces, Metrics, Dashboards and RUM. It renders
|
|
* in two modes:
|
|
* - autoApply=true (Logs, Traces, RUM) — a selection commits immediately, no Apply button.
|
|
* - autoApply=false (Metrics, Dashboards) — a selection stays pending until Apply is pressed.
|
|
*
|
|
* Clipboard note: `navigator.clipboard` needs a secure context. http://localhost and
|
|
* https:// origins qualify; a plain-HTTP remote host does not, and paste has no
|
|
* execCommand fallback (unlike copy). Both playwright configs already grant the
|
|
* clipboard-read / clipboard-write permissions.
|
|
*/
|
|
export class DateTimePickerPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// ==================== Locators ====================
|
|
|
|
// Trigger button — DateTime.vue's `dataTestName` prop, which defaults to
|
|
// "date-time-btn". Logs/Metrics/Dashboards all leave the default in place.
|
|
this.triggerBtn = page.locator('[data-test="date-time-btn"]').first();
|
|
|
|
// Picker panel — an id, not a data-test, in the component source.
|
|
this.panel = page.locator('#date-time-menu');
|
|
|
|
// Tab pair — absent when the host passes `disable-relative`.
|
|
this.relativeTab = page.locator('[data-test="date-time-relative-tab"]');
|
|
this.absoluteTab = page.locator('[data-test="date-time-absolute-tab"]');
|
|
|
|
// Copy / paste controls — added by PR #13374.
|
|
this.copyBtn = page.locator('[data-test="date-time-copy-btn"]');
|
|
this.pasteBtn = page.locator('[data-test="date-time-paste-btn"]');
|
|
|
|
// Shift buttons — Previous / Next flank the trigger (rendered when
|
|
// hideRangeShift is false, the default). PR #14683 caps the forward
|
|
// shift at "now", so Next is disabled in Relative mode and whenever
|
|
// the absolute window's end is within one second of the present.
|
|
this.prevShiftBtn = page.locator('[data-test="date-time-prev-btn"]');
|
|
this.nextShiftBtn = page.locator('[data-test="date-time-next-btn"]');
|
|
|
|
// Absolute panel fields. OTime renders the data-test on a role=group
|
|
// wrapper; the value lives on the descendant input[type=time].
|
|
this.startTimeInput = page.locator('[data-test="datetime-start-time"] input');
|
|
this.endTimeInput = page.locator('[data-test="datetime-end-time"] input');
|
|
|
|
// OTabPanels is used without `keep-alive`, so the inactive panel is
|
|
// unmounted (v-if). Presence of these is therefore a reliable signal of
|
|
// which tab is active — more robust than reading variant-driven classes.
|
|
// OTime stamps the forwarded data-test on BOTH its outer wrapper and its
|
|
// inner role=group element, so scope to the first to stay strict-mode safe.
|
|
this.absolutePanelMarker = page.locator('[data-test="datetime-start-time"]').first();
|
|
this.relativePanelMarker = page.locator('[data-test="date-time-relative-15-m-btn"]');
|
|
|
|
// Apply button — rendered only when the host sets autoApply=false.
|
|
this.applyBtn = page.locator('[data-test="date-time-apply-btn"]');
|
|
|
|
// Custom relative row — value input + unit select. The product DateTime.vue
|
|
// renders these controls WITHOUT a data-test (testability-only), so scope
|
|
// structurally: the custom row is the `.relative-row` that holds the native
|
|
// number input; the unit select is the single combobox inside that row.
|
|
this.customRow = page
|
|
.locator('#date-time-menu .relative-row')
|
|
.filter({ has: page.locator('input[type="number"]') });
|
|
this.customValueInput = this.customRow.locator('input[type="number"]');
|
|
this.customPeriodTrigger = this.customRow.locator('[role="combobox"]');
|
|
|
|
// ==================== Expected toast messages ====================
|
|
// Values of common.dateRangeCopied / dateRangePasted / dateRangePasteError
|
|
// in web/src/locales/languages/en-US.json.
|
|
this.copiedToastMessage = 'Date range copied';
|
|
this.pastedToastMessage = 'Date range pasted';
|
|
this.pasteErrorToastMessage = 'Could not parse date range';
|
|
}
|
|
|
|
// ==================== Locator factories ====================
|
|
|
|
/** OToast stamps the rendered message onto data-test-message. */
|
|
toastByMessage(message) {
|
|
return this.page.locator(`[data-test-message="${message}"]`);
|
|
}
|
|
|
|
relativePeriodBtn(suffix) {
|
|
return this.page.locator(`[data-test="date-time-relative-${suffix}-btn"]`);
|
|
}
|
|
|
|
/**
|
|
* Unit option in the custom period select; unit is one of s/m/h/d/w/M.
|
|
* The OSelect renders each option (ListboxItem/OSelectItem) with the option
|
|
* value mirrored onto `data-test-value`, so the option is addressable without
|
|
* the (absent) parent data-test. Only one select's dropdown is open at a time,
|
|
* so a bare `data-test-value` match is unambiguous.
|
|
*/
|
|
customPeriodOption(unit) {
|
|
return this.page.locator(`[data-test-value="${unit}"]`);
|
|
}
|
|
|
|
// ==================== Panel open / close ====================
|
|
|
|
async openPicker() {
|
|
await expect(this.triggerBtn).toBeVisible();
|
|
await this.triggerBtn.click();
|
|
await expect(this.panel).toBeVisible();
|
|
}
|
|
|
|
async expectPickerClosed() {
|
|
await expect(this.panel).toBeHidden();
|
|
}
|
|
|
|
/** Closes the panel without applying — Escape leaves any pending selection uncommitted. */
|
|
async closePickerWithEscape() {
|
|
await this.page.keyboard.press('Escape');
|
|
await expect(this.panel).toBeHidden();
|
|
}
|
|
|
|
// ==================== Tabs ====================
|
|
|
|
async selectRelativePeriod(suffix) {
|
|
const btn = this.relativePeriodBtn(suffix);
|
|
await expect(btn).toBeVisible();
|
|
await btn.click();
|
|
}
|
|
|
|
async expectAbsolutePanelActive() {
|
|
await expect(this.absolutePanelMarker).toBeVisible();
|
|
}
|
|
|
|
async expectRelativePanelActive() {
|
|
await expect(this.relativePanelMarker).toBeVisible();
|
|
}
|
|
|
|
// ==================== Copy / paste controls ====================
|
|
|
|
async expectCopyPasteControlsVisible() {
|
|
await expect(this.copyBtn).toBeVisible();
|
|
await expect(this.pasteBtn).toBeVisible();
|
|
}
|
|
|
|
async expectCopyPasteAriaLabels() {
|
|
await expect(this.copyBtn).toHaveAttribute('aria-label', 'Copy time range to clipboard');
|
|
await expect(this.pasteBtn).toHaveAttribute('aria-label', 'Paste time range');
|
|
}
|
|
|
|
async clickCopy() {
|
|
await expect(this.copyBtn).toBeVisible();
|
|
await this.copyBtn.click();
|
|
}
|
|
|
|
async clickPaste() {
|
|
await expect(this.pasteBtn).toBeVisible();
|
|
await this.pasteBtn.click();
|
|
}
|
|
|
|
// ==================== Shift buttons (Previous / Next) ====================
|
|
|
|
async expectShiftButtonsVisible() {
|
|
await expect(this.prevShiftBtn).toBeVisible();
|
|
await expect(this.nextShiftBtn).toBeVisible();
|
|
}
|
|
|
|
async expectShiftAriaLabels() {
|
|
await expect(this.prevShiftBtn).toHaveAttribute('aria-label', 'Previous');
|
|
await expect(this.nextShiftBtn).toHaveAttribute('aria-label', 'Next');
|
|
}
|
|
|
|
async expectNextEnabled() {
|
|
await expect(this.nextShiftBtn).toBeEnabled();
|
|
}
|
|
|
|
async expectNextDisabled() {
|
|
await expect(this.nextShiftBtn).toBeDisabled();
|
|
}
|
|
|
|
async expectPrevEnabled() {
|
|
await expect(this.prevShiftBtn).toBeEnabled();
|
|
}
|
|
|
|
async clickNext() {
|
|
await expect(this.nextShiftBtn).toBeEnabled();
|
|
await this.nextShiftBtn.click();
|
|
}
|
|
|
|
async clickPrev() {
|
|
await expect(this.prevShiftBtn).toBeEnabled();
|
|
await this.prevShiftBtn.click();
|
|
}
|
|
|
|
// ==================== Clipboard helpers ====================
|
|
|
|
/**
|
|
* Seeds the clipboard from inside the page. Chromium's clipboard API refuses
|
|
* to run against an unfocused document, so bring the tab forward first.
|
|
*/
|
|
async seedClipboard(text) {
|
|
await this.page.bringToFront();
|
|
await this.page.evaluate((value) => navigator.clipboard.writeText(value), text);
|
|
}
|
|
|
|
async readClipboard() {
|
|
await this.page.bringToFront();
|
|
return await this.page.evaluate(() => navigator.clipboard.readText());
|
|
}
|
|
|
|
/**
|
|
* Reads the clipboard and parses the {"start_date":micros,"end_date":micros}
|
|
* payload that Copy writes. Fails loudly if the payload is not that shape.
|
|
*/
|
|
async readCopiedRange() {
|
|
const raw = await this.readClipboard();
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`Clipboard did not contain JSON. Got: ${raw}`);
|
|
}
|
|
expect(typeof payload.start_date, `start_date in ${raw}`).toBe('number');
|
|
expect(typeof payload.end_date, `end_date in ${raw}`).toBe('number');
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* Pastes `text` and returns the range the picker resolved it to, read back
|
|
* through Copy. Copy resolves the selection through the picker's own timezone,
|
|
* so this round-trip is exact for second-aligned epoch input and free of any
|
|
* dependence on the runner's timezone.
|
|
*/
|
|
async pasteAndReadBackRange(text) {
|
|
await this.seedClipboard(text);
|
|
await this.clickPaste();
|
|
await this.expectPasteSuccessToast();
|
|
await this.expectAbsolutePanelActive();
|
|
await this.clickCopy();
|
|
return await this.readCopiedRange();
|
|
}
|
|
|
|
// ==================== Toast assertions ====================
|
|
|
|
async expectCopySuccessToast() {
|
|
await expect(this.toastByMessage(this.copiedToastMessage)).toBeVisible();
|
|
}
|
|
|
|
async expectPasteSuccessToast() {
|
|
await expect(this.toastByMessage(this.pastedToastMessage)).toBeVisible();
|
|
}
|
|
|
|
async expectPasteErrorToast() {
|
|
await expect(this.toastByMessage(this.pasteErrorToastMessage)).toBeVisible();
|
|
}
|
|
|
|
// ==================== Value readers ====================
|
|
|
|
async getStartTimeValue() {
|
|
return await this.startTimeInput.inputValue();
|
|
}
|
|
|
|
async getEndTimeValue() {
|
|
return await this.endTimeInput.inputValue();
|
|
}
|
|
|
|
async expectAbsoluteTimeFields(expectedStart, expectedEnd) {
|
|
await expect(this.startTimeInput).toHaveValue(expectedStart);
|
|
await expect(this.endTimeInput).toHaveValue(expectedEnd);
|
|
}
|
|
|
|
async getTriggerLabel() {
|
|
return (await this.triggerBtn.innerText()).trim();
|
|
}
|
|
|
|
// ==================== Custom relative value ====================
|
|
|
|
/** Fills the custom value input and blurs so the clamp commits. */
|
|
async setCustomValue(value) {
|
|
await expect(this.customValueInput).toBeVisible();
|
|
await this.customValueInput.fill(String(value));
|
|
await this.customValueInput.blur();
|
|
}
|
|
|
|
/** Focuses (snapshots lastValidCustomValue), empties, then blurs to restore. */
|
|
async clearCustomValue() {
|
|
await expect(this.customValueInput).toBeVisible();
|
|
await this.customValueInput.fill('');
|
|
await this.customValueInput.blur();
|
|
}
|
|
|
|
async expectCustomValue(expected) {
|
|
await expect(this.customValueInput).toHaveValue(String(expected));
|
|
}
|
|
|
|
/** Opens the custom period select and picks a unit (s/m/h/d/w/M). */
|
|
async selectCustomPeriod(unit) {
|
|
await expect(this.customPeriodTrigger).toBeVisible();
|
|
await this.customPeriodTrigger.click();
|
|
await expect(this.customPeriodOption(unit)).toBeVisible();
|
|
await this.customPeriodOption(unit).click();
|
|
}
|
|
|
|
/**
|
|
* Opens the period select, asserts which units are offered (present) and
|
|
* dropped (absent), then selects `unit` — all in one popover open so the
|
|
* absent checks are never satisfied by a not-yet-open popover. The
|
|
* `toHaveCount(0)` on dropped units also acts as the deterministic settle
|
|
* gate while the stream's max_query_range restriction propagates.
|
|
*/
|
|
async selectCustomPeriodExpectingUnits(unit, presentUnits, absentUnits) {
|
|
await expect(this.customPeriodTrigger).toBeVisible();
|
|
await this.customPeriodTrigger.click();
|
|
for (const u of presentUnits) {
|
|
await expect(this.customPeriodOption(u)).toHaveCount(1);
|
|
}
|
|
for (const u of absentUnits) {
|
|
await expect(this.customPeriodOption(u)).toHaveCount(0);
|
|
}
|
|
await expect(this.customPeriodOption(unit)).toBeVisible();
|
|
await this.customPeriodOption(unit).click();
|
|
}
|
|
|
|
async expectPresetDisabled(suffix) {
|
|
await expect(this.relativePeriodBtn(suffix)).toBeDisabled();
|
|
}
|
|
|
|
/**
|
|
* Waits for the transient pre-load restriction to clear. Before the selected
|
|
* stream's fields settle, `queryRangeRestrictionInHour` is seeded at 100000,
|
|
* which stamps a `max` attribute onto the custom input; once it resolves to
|
|
* -1 (no restriction) the max is removed.
|
|
*/
|
|
async waitForNoRestriction() {
|
|
await expect(this.customValueInput).not.toHaveAttribute('max', /[0-9]+/);
|
|
}
|
|
|
|
// ==================== Manual-apply mode ====================
|
|
|
|
async expectApplyButtonVisible() {
|
|
await expect(this.applyBtn).toBeVisible();
|
|
}
|
|
|
|
async clickApply() {
|
|
await expect(this.applyBtn).toBeVisible();
|
|
await this.applyBtn.click();
|
|
}
|
|
}
|
|
|
|
export default DateTimePickerPage;
|