mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 07:06:41 +00:00
Merge pull request #12521 from nocodb/nc-script-test
This commit is contained in:
103
tests/playwright/pages/Dashboard/Scripts/ConfigPanel.ts
Normal file
103
tests/playwright/pages/Dashboard/Scripts/ConfigPanel.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
tests/playwright/pages/Dashboard/Scripts/Playground.ts
Normal file
108
tests/playwright/pages/Dashboard/Scripts/Playground.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
114
tests/playwright/pages/Dashboard/Scripts/Topbar.ts
Normal file
114
tests/playwright/pages/Dashboard/Scripts/Topbar.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
95
tests/playwright/pages/Dashboard/Scripts/index.ts
Normal file
95
tests/playwright/pages/Dashboard/Scripts/index.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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({
|
async createEntity({
|
||||||
type,
|
type,
|
||||||
skipOpeningModal,
|
skipOpeningModal,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { CmdK } from './Command/CmdKPage';
|
|||||||
import { CmdL } from './Command/CmdLPage';
|
import { CmdL } from './Command/CmdLPage';
|
||||||
import { CalendarPage } from './Calendar';
|
import { CalendarPage } from './Calendar';
|
||||||
import { Extensions } from './Extensions';
|
import { Extensions } from './Extensions';
|
||||||
|
import { ScriptsPage } from './Scripts';
|
||||||
|
|
||||||
export class DashboardPage extends BasePage {
|
export class DashboardPage extends BasePage {
|
||||||
readonly base: any;
|
readonly base: any;
|
||||||
@@ -63,6 +64,7 @@ export class DashboardPage extends BasePage {
|
|||||||
readonly cmdK: CmdK;
|
readonly cmdK: CmdK;
|
||||||
readonly cmdL: CmdL;
|
readonly cmdL: CmdL;
|
||||||
readonly extensions: Extensions;
|
readonly extensions: Extensions;
|
||||||
|
readonly scripts: ScriptsPage;
|
||||||
|
|
||||||
constructor(rootPage: Page, base: any) {
|
constructor(rootPage: Page, base: any) {
|
||||||
super(rootPage);
|
super(rootPage);
|
||||||
@@ -97,6 +99,7 @@ export class DashboardPage extends BasePage {
|
|||||||
this.cmdK = new CmdK(this);
|
this.cmdK = new CmdK(this);
|
||||||
this.cmdL = new CmdL(this);
|
this.cmdL = new CmdL(this);
|
||||||
this.extensions = new Extensions(this);
|
this.extensions = new Extensions(this);
|
||||||
|
this.scripts = new ScriptsPage(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
Reference in New Issue
Block a user