Merge pull request #12521 from nocodb/nc-script-test

This commit is contained in:
Anbarasu
2025-10-22 16:50:23 +05:30
committed by GitHub
6 changed files with 441 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
import { expect, Locator } from '@playwright/test';
import BasePage from '../../Base';
import { ScriptsPage } from './index';
export class ScriptsConfigPanel extends BasePage {
readonly scriptsPage: ScriptsPage;
constructor(scriptsPage: ScriptsPage) {
super(scriptsPage.rootPage);
this.scriptsPage = scriptsPage;
}
get(): Locator {
return this.rootPage.getByTestId('nc-script-config-panel');
}
async verifyVisible(): Promise<void> {
await expect(this.get()).toBeVisible();
}
async verifyTitle(title: string): Promise<void> {
await expect(this.rootPage.getByTestId('nc-script-config-title')).toHaveText(title);
}
async verifyDescription(description: string): Promise<void> {
await expect(this.rootPage.getByTestId('nc-script-config-description')).toContainText(description);
}
async verifyWarningVisible(): Promise<void> {
await expect(this.rootPage.getByTestId('nc-script-config-warning')).toBeVisible();
}
async fillInput(
key: string,
value: string | number,
type?: 'text' | 'number' | 'select' | 'table' | 'view' | 'field'
): Promise<void> {
if (!type) {
// Try to find which input exists
const textInput = this.rootPage.getByTestId(`nc-script-config-text-${key}`);
const numberInput = this.rootPage.getByTestId(`nc-script-config-number-${key}`);
const selectInput = this.rootPage.getByTestId(`nc-script-config-select-${key}`);
const tableInput = this.rootPage.getByTestId(`nc-script-config-table-${key}`);
const viewInput = this.rootPage.getByTestId(`nc-script-config-view-${key}`);
const fieldInput = this.rootPage.getByTestId(`nc-script-config-field-${key}`);
if (await textInput.isVisible().catch(() => false)) {
type = 'text';
} else if (await numberInput.isVisible().catch(() => false)) {
type = 'number';
} else if (await selectInput.isVisible().catch(() => false)) {
type = 'select';
} else if (await tableInput.isVisible().catch(() => false)) {
type = 'table';
} else if (await viewInput.isVisible().catch(() => false)) {
type = 'view';
} else if (await fieldInput.isVisible().catch(() => false)) {
type = 'field';
}
}
switch (type) {
case 'text':
await this.rootPage.getByTestId(`nc-script-config-text-${key}`).fill(value.toString());
break;
case 'number':
await this.rootPage.getByTestId(`nc-script-config-number-${key}`).fill(value.toString());
break;
case 'select':
await this.rootPage.getByTestId(`nc-script-config-select-${key}`).click();
await this.rootPage.waitForTimeout(300);
await this.rootPage
.locator(`.ant-select-dropdown:visible .ant-select-item`)
.filter({ hasText: value.toString() })
.first()
.click();
await this.rootPage.waitForTimeout(300);
break;
case 'table':
case 'view':
case 'field': {
// Click the selector to open dropdown
await this.rootPage.getByTestId(`nc-script-config-${type}-${key}`).click();
await this.rootPage.waitForTimeout(500);
// Find and click the item in the NcList dropdown
const dropdown = this.rootPage.locator('.nc-dropdown-list-wrapper:visible, .nc-list-wrapper:visible');
await dropdown.waitFor({ state: 'visible', timeout: 5000 });
// Click the item by text
const item = dropdown.locator('.nc-list-item').filter({ hasText: value.toString() }).first();
await item.click();
await this.rootPage.waitForTimeout(300);
break;
}
}
}
async save(): Promise<void> {
await this.rootPage.getByTestId('nc-script-config-save-btn').click();
await this.rootPage.waitForTimeout(1000);
}
}

View File

@@ -0,0 +1,108 @@
import { expect, Locator } from '@playwright/test';
import BasePage from '../../Base';
import { ScriptsPage } from './index';
export class ScriptsPlayground extends BasePage {
readonly scriptsPage: ScriptsPage;
constructor(scriptsPage: ScriptsPage) {
super(scriptsPage.rootPage);
this.scriptsPage = scriptsPage;
}
get(): Locator {
return this.rootPage.locator('.nc-playground-container').first();
}
async verifyTextOutput(text: string): Promise<void> {
const textItem = this.rootPage.getByTestId('nc-playground-text-output').filter({ hasText: text });
await expect(textItem).toBeVisible({ timeout: 10000 });
}
async verifyMarkdownOutput(text: string): Promise<void> {
const markdownItem = this.rootPage.getByTestId('nc-playground-markdown-output').filter({ hasText: text });
await expect(markdownItem).toBeVisible({ timeout: 10000 });
}
async verifyTableOutput(): Promise<void> {
const tableItem = this.rootPage.getByTestId('nc-playground-table-output');
await expect(tableItem).toBeVisible({ timeout: 10000 });
}
async verifyInspectOutput(): Promise<void> {
const inspectItem = this.rootPage.getByTestId('nc-playground-inspect-output');
await expect(inspectItem).toBeVisible({ timeout: 10000 });
}
// Input interaction methods
async fillTextInput(label: string, value: string): Promise<void> {
const inputContainer = this.rootPage.getByTestId('nc-playground-input');
await inputContainer.waitFor({ state: 'visible', timeout: 10000 });
const textInput = inputContainer.locator('input[type="text"], textarea').first();
await textInput.fill(value);
const submitButton = inputContainer.locator('button').first();
await submitButton.click();
await this.rootPage.waitForTimeout(500);
}
async clickButton(buttonLabel: string): Promise<void> {
const inputContainer = this.rootPage.getByTestId('nc-playground-input');
await inputContainer.waitFor({ state: 'visible', timeout: 10000 });
const button = inputContainer.locator(`button:has-text("${buttonLabel}")`);
await button.click();
await this.rootPage.waitForTimeout(500);
}
async selectOption(optionLabel: string): Promise<void> {
const inputContainer = this.rootPage.getByTestId('nc-playground-input');
await inputContainer.waitFor({ state: 'visible', timeout: 10000 });
const select = inputContainer.locator('select, .ant-select').first();
await select.click();
await this.rootPage.waitForTimeout(300);
const option = this.rootPage.locator(`.ant-select-dropdown:visible .ant-select-item:has-text("${optionLabel}")`);
await option.click();
await this.rootPage.waitForTimeout(500);
}
async uploadFile(filePath: string): Promise<void> {
const inputContainer = this.rootPage.getByTestId('nc-playground-input');
await inputContainer.waitFor({ state: 'visible', timeout: 10000 });
const fileInput = inputContainer.locator('input[type="file"]');
await fileInput.setInputFiles(filePath);
await this.rootPage.waitForTimeout(1000);
}
// Generic output verification
async verifyOutputContains(text: string): Promise<void> {
await expect(this.get()).toContainText(text, { timeout: 10000 });
}
async verifyOutputNotContains(text: string): Promise<void> {
await expect(this.get()).not.toContainText(text);
}
// Verify playground is cleared (no items)
async verifyClear(): Promise<void> {
// After clear, playground items should not exist
const playgroundItems = this.rootPage.locator('[data-testid^="nc-playground-item-"]');
await expect(playgroundItems).toHaveCount(0);
}
// Verify empty state message
async verifyEmptyState(): Promise<void> {
const emptyMessage = this.rootPage.getByTestId('nc-playground-empty');
await expect(emptyMessage).toBeVisible();
}
// Workflow step verification
async verifyWorkflowStep(title: string): Promise<void> {
const workflowStep = this.get().locator('.workflow-step-card .step-header', { hasText: title });
await expect(workflowStep).toBeVisible({ timeout: 10000 });
}
}

View File

@@ -0,0 +1,114 @@
import { expect, Locator } from '@playwright/test';
import BasePage from '../../Base';
import { ScriptsPage } from './index';
export class ScriptsTopbar extends BasePage {
readonly scriptsPage: ScriptsPage;
constructor(scriptsPage: ScriptsPage) {
super(scriptsPage.rootPage);
this.scriptsPage = scriptsPage;
}
// Settings button
getSettingsButton(): Locator {
return this.rootPage.getByTestId('nc-script-settings-btn');
}
async clickSettings(): Promise<void> {
await this.getSettingsButton().click();
await this.rootPage.waitForTimeout(500);
}
async verifySettingsButtonVisible(): Promise<void> {
await expect(this.getSettingsButton()).toBeVisible();
}
async verifySettingsButtonActive(isActive: boolean): Promise<void> {
const button = this.rootPage.getByTestId('nc-script-settings-btn');
if (isActive) {
await expect(button).toHaveClass(/is-settings-open/);
} else {
await expect(button).not.toHaveClass(/is-settings-open/);
}
}
// Run button
getRunButton(): Locator {
return this.rootPage.getByTestId('nc-script-run-btn');
}
async clickRun(): Promise<void> {
await this.getRunButton().click();
await this.rootPage.waitForTimeout(500);
}
async verifyRunButtonVisible(): Promise<void> {
await expect(this.getRunButton()).toBeVisible();
}
async verifyRunButtonEnabled(): Promise<void> {
await expect(this.getRunButton()).toBeEnabled();
}
async verifyRunButtonState(enabled: boolean): Promise<void> {
if (enabled) {
await expect(this.getRunButton()).toBeEnabled();
} else {
await expect(this.getRunButton()).toBeDisabled();
}
}
// Stop button
getStopButton(): Locator {
return this.rootPage.getByTestId('nc-script-stop-btn');
}
async clickStop(): Promise<void> {
await this.getStopButton().click();
await this.rootPage.waitForTimeout(500);
}
async verifyStopButtonVisible(): Promise<void> {
await expect(this.getStopButton()).toBeVisible();
}
// Restart button
getRestartButton(): Locator {
return this.rootPage.getByTestId('nc-script-restart-btn');
}
async clickRestart(): Promise<void> {
await this.getRestartButton().click();
await this.rootPage.waitForTimeout(500);
}
async verifyScriptState({ state }: { state: 'running' | 'stopped' }) {
const elem = this.rootPage.getByTestId('nc-script-running-indicator');
if (state === 'running') {
await expect(elem).toBeVisible();
} else {
await expect(elem).not.toBeVisible();
}
}
// Wait for execution to complete
async waitForExecutionComplete(timeout: number = 10000): Promise<void> {
await this.rootPage.waitForTimeout(500);
// Wait for running indicator to disappear
const runningIndicator = this.rootPage.getByTestId('nc-script-running-indicator');
await runningIndicator.waitFor({ state: 'hidden', timeout });
await this.rootPage.waitForTimeout(500);
}
async runScript(): Promise<void> {
await this.clickRun();
await this.waitForExecutionComplete();
}
get(): Locator {
return this.rootPage.locator('.nc-table-topbar');
}
}

View File

@@ -0,0 +1,95 @@
import { expect } from '@playwright/test';
import BasePage from '../../Base';
import { DashboardPage } from '..';
import { ScriptsTopbar } from './Topbar';
import { ScriptsConfigPanel } from './ConfigPanel';
import { ScriptsPlayground } from './Playground';
export class ScriptsPage extends BasePage {
readonly dashboardPage: DashboardPage;
readonly topbar: ScriptsTopbar;
readonly configPanel: ScriptsConfigPanel;
readonly playground: ScriptsPlayground;
constructor(dashboard: DashboardPage) {
super(dashboard.rootPage);
this.dashboardPage = dashboard;
this.topbar = new ScriptsTopbar(this);
this.configPanel = new ScriptsConfigPanel(this);
this.playground = new ScriptsPlayground(this);
}
get() {
return this.dashboardPage.get().locator('.nc-scripts-content-resizable-wrapper');
}
getEditor() {
return this.rootPage.getByTestId('nc-scripts-editor');
}
async isEditorVisible(): Promise<boolean> {
return await this.getEditor().isVisible();
}
async getEditorContent(): Promise<string> {
const editorContainer = this.getEditor();
const content = await editorContainer.getAttribute('data-code');
return content || '';
}
async verifyEditorHasContent(): Promise<void> {
const content = await this.getEditorContent();
expect(content.length).toBeGreaterThan(0);
}
async setEditorContent(
content: string,
scriptId: string,
workspaceId: string,
baseId: string,
api: any
): Promise<void> {
await api.internal.postOperation(
workspaceId,
baseId,
{
operation: 'updateScript',
},
{
id: scriptId,
script: content,
}
);
await this.rootPage.waitForTimeout(500);
await this.dashboardPage.waitForLoaderToDisappear();
}
async verifyEditorContentContains(text: string): Promise<void> {
const content = await this.getEditorContent();
expect(content).toContain(text);
}
getBottomBar() {
return this.dashboardPage.get().locator('.h-9.border-t-1');
}
async toggleEditor(): Promise<void> {
const toggleButton = this.getBottomBar().locator('button').first();
await toggleButton.click();
await this.rootPage.waitForTimeout(300);
}
async verifyEditorToggleState(isOpen: boolean): Promise<void> {
const toggleButton = this.getBottomBar().locator('button').first();
if (isOpen) {
await expect(toggleButton).toHaveClass(/bg-nc-bg-brand/);
} else {
await expect(toggleButton).not.toHaveClass(/bg-nc-bg-brand/);
}
}
async isPlaygroundVisible(): Promise<boolean> {
return await this.playground.get().isVisible();
}
}

View File

@@ -161,6 +161,24 @@ export class TreeViewPage extends BasePage {
}
}
async openScript({ title, baseTitle }: { title: string; baseTitle?: string }) {
if (baseTitle) {
await this.dashboard.sidebar.baseNode.verifyActiveProject({ baseTitle, open: true });
}
const scriptNode = this.get().getByTestId(`view-sidebar-script-${title}`);
await scriptNode.waitFor({ state: 'visible' });
await scriptNode.click({});
// todo: remove this after fixing the issue
await this.rootPage.waitForTimeout(1000);
await scriptNode.click({
// x:10, y:10
});
}
async createEntity({
type,
skipOpeningModal,

View File

@@ -29,6 +29,7 @@ import { CmdK } from './Command/CmdKPage';
import { CmdL } from './Command/CmdLPage';
import { CalendarPage } from './Calendar';
import { Extensions } from './Extensions';
import { ScriptsPage } from './Scripts';
export class DashboardPage extends BasePage {
readonly base: any;
@@ -63,6 +64,7 @@ export class DashboardPage extends BasePage {
readonly cmdK: CmdK;
readonly cmdL: CmdL;
readonly extensions: Extensions;
readonly scripts: ScriptsPage;
constructor(rootPage: Page, base: any) {
super(rootPage);
@@ -97,6 +99,7 @@ export class DashboardPage extends BasePage {
this.cmdK = new CmdK(this);
this.cmdL = new CmdL(this);
this.extensions = new Extensions(this);
this.scripts = new ScriptsPage(this);
}
get() {