mirror of https://github.com/Squidex/squidex.git
Browse Source
* Basic test * Run on playwright branch. * Another test * Fix tests again? * Make screenshot. * Update snapshots. * Add new workflow. * Disable onboarding. * Run workflow once. * Add linux snapshots. * Fix URLs * More tests. * More tests.pull/1049/head
committed by
GitHub
78 changed files with 4365 additions and 141 deletions
@ -0,0 +1,57 @@ |
|||
name: Screenshot |
|||
concurrency: build |
|||
|
|||
on: |
|||
workflow_dispatch: |
|||
|
|||
jobs: |
|||
screenshot: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Prepare - Checkout |
|||
uses: actions/checkout@v4.1.1 |
|||
|
|||
- name: Prepare - Setup QEMU |
|||
uses: docker/setup-qemu-action@v3.0.0 |
|||
|
|||
- name: Prepare - Setup Docker Buildx |
|||
uses: docker/setup-buildx-action@v3.0.0 |
|||
|
|||
- name: Prepare - Setup Node |
|||
uses: actions/setup-node@v3 |
|||
with: |
|||
node-version: 18 |
|||
|
|||
- name: Build - BUILD |
|||
uses: docker/build-push-action@v5.1.0 |
|||
with: |
|||
load: true |
|||
cache-from: type=gha |
|||
cache-to: type=gha,mode=max |
|||
tags: squidex-local |
|||
|
|||
- name: Test - Start Compose |
|||
run: docker-compose up -d |
|||
working-directory: tools/TestSuite |
|||
|
|||
- name: Test - Install Playwright Dependencies |
|||
run: npm ci |
|||
working-directory: './tools/e2e' |
|||
|
|||
- name: Test - Install Playwright Browsers |
|||
run: npx playwright install --with-deps |
|||
working-directory: './tools/e2e' |
|||
|
|||
- name: Test - Run Playwright Tests |
|||
run: npx playwright test --update-snapshots |
|||
working-directory: './tools/e2e' |
|||
env: |
|||
BASE__URL: http://localhost:8080 |
|||
|
|||
- name: Test - Upload Playwright Artifacts |
|||
if: always() |
|||
uses: actions/upload-artifact@v3 |
|||
with: |
|||
name: snapshots |
|||
path: tools/e2e/snapshots/ |
|||
retention-days: 30 |
|||
@ -0,0 +1,73 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Net; |
|||
using TestSuite.Fixtures; |
|||
|
|||
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
|||
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
|
|||
|
|||
namespace TestSuite.ApiTests; |
|||
|
|||
public class StatisticsTests : IClassFixture<CreatedAppFixture> |
|||
{ |
|||
public CreatedAppFixture _ { get; } |
|||
|
|||
public StatisticsTests(CreatedAppFixture fixture) |
|||
{ |
|||
_ = fixture; |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_logs() |
|||
{ |
|||
// STEP 1: Get initial log response.
|
|||
var log = await _.Client.Statistics.GetLogAsync(); |
|||
|
|||
|
|||
// STEP 2: Download log.
|
|||
var httpClient = _.Client.CreateHttpClient(); |
|||
|
|||
var response = await httpClient.GetAsync(log.DownloadUrl); |
|||
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode); |
|||
Assert.Equal("text/csv", response.Content.Headers.GetValues("Content-Type").First()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_api_calls() |
|||
{ |
|||
// STEP 1: Get statistics.
|
|||
var dateFrom = DateTimeOffset.UtcNow.AddDays(-30); |
|||
var dateTo = DateTimeOffset.UtcNow; |
|||
|
|||
var result = await _.Client.Statistics.GetUsagesAsync(dateFrom, dateTo); |
|||
|
|||
Assert.NotNull(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_storage_size() |
|||
{ |
|||
// STEP 1: Get statistics.
|
|||
var dateFrom = DateTimeOffset.UtcNow.AddDays(-30); |
|||
var dateTo = DateTimeOffset.UtcNow; |
|||
|
|||
var result = await _.Client.Statistics.GetStorageSizesAsync(dateFrom, dateTo); |
|||
|
|||
Assert.NotNull(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_current_storage_size() |
|||
{ |
|||
// STEP 1: Get statistics.
|
|||
var result = await _.Client.Statistics.GetCurrentStorageSizeAsync(); |
|||
|
|||
Assert.NotNull(result); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.ClientLibrary; |
|||
using TestSuite.Fixtures; |
|||
|
|||
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
|||
|
|||
namespace TestSuite.ApiTests; |
|||
|
|||
public class TeamCreationTests : IClassFixture<ClientFixture> |
|||
{ |
|||
private readonly string teamName = Guid.NewGuid().ToString(); |
|||
|
|||
public ClientFixture _ { get; } |
|||
|
|||
public TeamCreationTests(ClientFixture fixture) |
|||
{ |
|||
_ = fixture; |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_create_team() |
|||
{ |
|||
var request = new CreateTeamDto |
|||
{ |
|||
Name = teamName |
|||
}; |
|||
|
|||
var team = await _.Client.Teams.PostTeamAsync(request); |
|||
|
|||
Assert.Equal(teamName, team.Name); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_create_team_with_duplicate_name() |
|||
{ |
|||
var request = new CreateTeamDto |
|||
{ |
|||
Name = teamName |
|||
}; |
|||
|
|||
var team1 = await _.Client.Teams.PostTeamAsync(request); |
|||
var team2 = await _.Client.Teams.PostTeamAsync(request); |
|||
|
|||
Assert.Equal(teamName, team1.Name); |
|||
Assert.Equal(teamName, team2.Name); |
|||
Assert.NotEqual(team1.Id, team2.Id); |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using TestSuite.Fixtures; |
|||
|
|||
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
|||
|
|||
namespace TestSuite.ApiTests; |
|||
|
|||
public class TeamStatisticsTests : IClassFixture<CreatedTeamFixture> |
|||
{ |
|||
public CreatedTeamFixture _ { get; } |
|||
|
|||
public TeamStatisticsTests(CreatedTeamFixture fixture) |
|||
{ |
|||
_ = fixture; |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_api_calls() |
|||
{ |
|||
// STEP 1: Get statistics.
|
|||
var dateFrom = DateTimeOffset.UtcNow.AddDays(-30); |
|||
var dateTo = DateTimeOffset.UtcNow; |
|||
|
|||
var result = await _.Client.Statistics.GetUsagesForTeamAsync(_.TeamId, dateFrom, dateTo); |
|||
|
|||
Assert.NotNull(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_storage_size() |
|||
{ |
|||
// STEP 1: Get statistics.
|
|||
var dateFrom = DateTimeOffset.UtcNow.AddDays(-30); |
|||
var dateTo = DateTimeOffset.UtcNow; |
|||
|
|||
var result = await _.Client.Statistics.GetStorageSizesForTeamAsync(_.TeamId, dateFrom, dateTo); |
|||
|
|||
Assert.NotNull(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_get_current_storage_size_for() |
|||
{ |
|||
// STEP 1: Get statistics.
|
|||
var result = await _.Client.Statistics.GetTeamCurrentStorageSizeForTeamAsync(_.TeamId); |
|||
|
|||
Assert.NotNull(result); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.ClientLibrary; |
|||
|
|||
namespace TestSuite.Fixtures; |
|||
|
|||
public class CreatedTeamFixture : ClientFixture |
|||
{ |
|||
public string TeamName { get; } = $"my-team-{Guid.NewGuid()}"; |
|||
|
|||
public string TeamId => Team.Id; |
|||
|
|||
public TeamDto Team { get; private set; } |
|||
|
|||
public override async Task InitializeAsync() |
|||
{ |
|||
await base.InitializeAsync(); |
|||
|
|||
await Factories.CreateAsync(TeamName, async () => |
|||
{ |
|||
try |
|||
{ |
|||
var request = new CreateTeamDto |
|||
{ |
|||
Name = TeamName |
|||
}; |
|||
|
|||
Team = await Client.Teams.PostTeamAsync(request); |
|||
} |
|||
catch (SquidexException ex) |
|||
{ |
|||
if (ex.StatusCode != 400) |
|||
{ |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
/* eslint-disable */ |
|||
|
|||
module.exports = { |
|||
"env": { |
|||
"browser": true, |
|||
"node": true |
|||
}, |
|||
"extends": [ |
|||
"airbnb-typescript/base" |
|||
], |
|||
"parser": "@typescript-eslint/parser", |
|||
"parserOptions": { |
|||
"project": "tsconfig.json" |
|||
}, |
|||
"plugins": [ |
|||
"deprecation", |
|||
"eslint-plugin-import", |
|||
"@typescript-eslint", |
|||
], |
|||
"rules": { |
|||
"deprecation/deprecation": "warn", |
|||
"@typescript-eslint/dot-notation": "off", |
|||
"@typescript-eslint/indent": "off", |
|||
"@typescript-eslint/lines-between-class-members": "off", |
|||
"@typescript-eslint/member-delimiter-style": [ |
|||
"error", |
|||
{ |
|||
"multiline": { |
|||
"delimiter": "semi", |
|||
"requireLast": true |
|||
}, |
|||
"singleline": { |
|||
"delimiter": "semi", |
|||
"requireLast": false |
|||
} |
|||
} |
|||
], |
|||
"@typescript-eslint/naming-convention": [ |
|||
"error", |
|||
{ |
|||
"selector": "variable", |
|||
"format": [ |
|||
"camelCase", |
|||
"PascalCase", |
|||
"UPPER_CASE", |
|||
], |
|||
"leadingUnderscore": "allow", |
|||
"trailingUnderscore": "allow", |
|||
}, |
|||
{ |
|||
"selector": "typeLike", |
|||
"format": [ |
|||
"PascalCase" |
|||
], |
|||
} |
|||
], |
|||
"@typescript-eslint/no-this-alias": "error", |
|||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", |
|||
"@typescript-eslint/no-unused-expressions": "off", |
|||
"@typescript-eslint/no-use-before-define": "off", |
|||
"@typescript-eslint/no-shadow": "off", |
|||
"@typescript-eslint/no-unused-vars": [ |
|||
"error", |
|||
{ |
|||
"argsIgnorePattern": "^_", |
|||
"varsIgnorePattern": "^_" |
|||
} |
|||
], |
|||
"@typescript-eslint/return-await": "off", |
|||
"@typescript-eslint/quotes": [ |
|||
"error", |
|||
"single" |
|||
], |
|||
"@typescript-eslint/semi": [ |
|||
"error", |
|||
"always" |
|||
], |
|||
"arrow-body-style": "off", |
|||
"arrow-parens": "off", |
|||
"class-methods-use-this": "off", |
|||
"default-case": "off", |
|||
"function-paren-newline": "off", |
|||
"implicit-arrow-linebreak": "off", |
|||
"import/extensions": "off", |
|||
"import/no-extraneous-dependencies": "off", |
|||
"import/no-useless-path-segments": "off", |
|||
"import/order": ["error", { |
|||
"pathGroupsExcludedImportTypes": ["builtin"], |
|||
"pathGroups": [{ |
|||
"pattern": "@app/**", |
|||
"group": "external", |
|||
"position": "after" |
|||
}], |
|||
"alphabetize": { |
|||
"order": "asc" |
|||
} |
|||
}], |
|||
"import/prefer-default-export": "off", |
|||
"linebreak-style": "off", |
|||
"max-classes-per-file": "off", |
|||
"max-len": "off", |
|||
"newline-per-chained-call": "off", |
|||
"no-else-return": "off", |
|||
"no-mixed-operators": "off", |
|||
"no-nested-ternary": "off", |
|||
"no-param-reassign": "off", |
|||
"no-plusplus": "off", |
|||
"no-prototype-builtins": "off", |
|||
"no-restricted-syntax": "off", |
|||
"no-trailing-spaces": "error", |
|||
"no-underscore-dangle": "off", |
|||
"object-curly-newline": [ |
|||
"error", |
|||
{ |
|||
"ObjectExpression": { |
|||
"consistent": true |
|||
}, |
|||
"ObjectPattern": { |
|||
"consistent": true |
|||
}, |
|||
"ImportDeclaration": "never", |
|||
"ExportDeclaration": "never" |
|||
} |
|||
], |
|||
"operator-linebreak": "off", |
|||
"prefer-destructuring": "off", |
|||
"sort-imports": [ |
|||
"error", |
|||
{ |
|||
"ignoreCase": true, |
|||
"ignoreDeclarationSort": true |
|||
} |
|||
], |
|||
} |
|||
}; |
|||
@ -0,0 +1,10 @@ |
|||
/blob-report/ |
|||
|
|||
/playwright/ |
|||
/playwright-report/ |
|||
/playwright/.cache/ |
|||
|
|||
/test-results/ |
|||
/tests-examples/ |
|||
|
|||
node_modules/ |
|||
File diff suppressed because it is too large
@ -0,0 +1,24 @@ |
|||
{ |
|||
"name": "e2e", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"lint": "eslint tests/**/*.ts", |
|||
"test": "playwright test --ui", |
|||
"test:ci": "playwright test" |
|||
}, |
|||
"keywords": [], |
|||
"author": "Sebastian", |
|||
"license": "ISC", |
|||
"devDependencies": { |
|||
"@playwright/test": "^1.40.0", |
|||
"@types/node": "^20.9.3", |
|||
"@typescript-eslint/eslint-plugin": "^6.12.0", |
|||
"@typescript-eslint/parser": "^6.12.0", |
|||
"eslint": "^8.54.0", |
|||
"eslint-config-airbnb-typescript": "17.1.0", |
|||
"eslint-plugin-deprecation": "^2.0.0", |
|||
"eslint-plugin-import": "2.29.0" |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
import path from 'path'; |
|||
import { defineConfig, devices } from '@playwright/test'; |
|||
|
|||
export const TEMPORARY_PATH = path.join(__dirname, 'playwright/.temp'); |
|||
|
|||
export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json'); |
|||
|
|||
/** |
|||
* See https://playwright.dev/docs/test-configuration.
|
|||
*/ |
|||
export default defineConfig({ |
|||
testDir: './tests', |
|||
/* Run tests in files in parallel */ |
|||
fullyParallel: true, |
|||
/* Fail the build on CI if you accidentally left test.only in the source code. */ |
|||
forbidOnly: !!process.env.CI, |
|||
/* Use a dedicated folder for snapshots. */ |
|||
snapshotDir: './snapshots', |
|||
/* Retry on CI only */ |
|||
retries: process.env.CI ? 0 : 0, |
|||
/* Opt out of parallel tests on CI. */ |
|||
workers: process.env.CI ? 1 : undefined, |
|||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ |
|||
reporter: 'html', |
|||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ |
|||
use: { |
|||
/* Base URL to use in actions like `await page.goto('/')`. */ |
|||
baseURL: process.env.BASE__URL || 'https://localhost:5001', |
|||
|
|||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ |
|||
trace: 'on-first-retry', |
|||
}, |
|||
|
|||
/* Configure projects for major browsers */ |
|||
projects: [ |
|||
{ |
|||
name: 'login', |
|||
testMatch: 'tests/given-login/_setup.ts', |
|||
}, |
|||
|
|||
{ |
|||
name: 'app', |
|||
testMatch: 'tests/given-app/_setup.ts', |
|||
dependencies: ['login'], |
|||
use: { |
|||
storageState: STORAGE_STATE, |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
name: 'given login', |
|||
testMatch: 'tests/given-login/*.spec.ts', |
|||
dependencies: ['login'], |
|||
use: { |
|||
...devices['Desktop Chrome'], |
|||
storageState: STORAGE_STATE, |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
name: 'given app', |
|||
testMatch: 'tests/given-app/*.spec.ts', |
|||
dependencies: ['app'], |
|||
use: { |
|||
...devices['Desktop Chrome'], |
|||
storageState: STORAGE_STATE, |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
name: 'logged out', |
|||
testMatch: 'tests/*.spec.ts', |
|||
use: { |
|||
...devices['Desktop Chrome'], |
|||
}, |
|||
}, |
|||
], |
|||
}); |
|||
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 35 KiB |
@ -0,0 +1,40 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { test as base, Page } from '@playwright/test'; |
|||
|
|||
type BaseFixture = { |
|||
dropdown: Dropdown; |
|||
}; |
|||
|
|||
class Dropdown { |
|||
constructor( |
|||
private readonly page: Page, |
|||
) { |
|||
} |
|||
|
|||
public async delete() { |
|||
await this.page.getByText('Delete').click(); |
|||
await this.page.getByRole('button', { name: /Yes/ }).click(); |
|||
} |
|||
|
|||
public async action(name: string) { |
|||
await this.page.getByText(name).click(); |
|||
await this.page.locator('sqx-dropdown-menu').waitFor({ state: 'hidden' }); |
|||
} |
|||
} |
|||
|
|||
export const test = base.extend<BaseFixture>({ |
|||
dropdown: async ({ page }, use) => { |
|||
const dropdown = new Dropdown(page); |
|||
|
|||
await use(dropdown); |
|||
}, |
|||
}); |
|||
|
|||
export { expect } from '@playwright/test'; |
|||
|
|||
@ -0,0 +1,24 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { readJsonAsync } from '../utils'; |
|||
import { test as base } from './../given-login/_fixture'; |
|||
export { expect } from '@playwright/test'; |
|||
|
|||
type AppFixture = { |
|||
appName: string; |
|||
}; |
|||
|
|||
export const test = base.extend<{}, AppFixture>({ |
|||
appName: [async ({}, use) => { |
|||
const config = await readJsonAsync<AppFixture>('app', null!); |
|||
|
|||
await use(config.appName); |
|||
}, { scope: 'worker', auto: true }], |
|||
}); |
|||
|
|||
|
|||
@ -0,0 +1,24 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { test as setup } from '@playwright/test'; |
|||
import { getRandomId, writeJsonAsync } from '../utils'; |
|||
|
|||
setup('prepare app', async ({ page }) => { |
|||
const appName = `my-app-${getRandomId()}`; |
|||
|
|||
await page.goto('/app'); |
|||
|
|||
await page.getByTestId('new-app').click(); |
|||
|
|||
await page.locator('#name').fill(appName); |
|||
await page.getByRole('button', { name: 'Create' }).click(); |
|||
|
|||
await page.getByRole('heading', { name: appName }).click(); |
|||
|
|||
await writeJsonAsync('app', { appName }); |
|||
}); |
|||
@ -0,0 +1,16 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { expect, test } from './_fixture'; |
|||
|
|||
test('visual test', async ({ page, appName }) => { |
|||
await page.goto(`/app/${appName}`); |
|||
|
|||
await page.waitForLoadState('networkidle'); |
|||
|
|||
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.05 }); |
|||
}); |
|||
@ -0,0 +1,89 @@ |
|||
import { expect, Page } from '@playwright/test'; |
|||
import { escapeRegex, getRandomId } from '../utils'; |
|||
import { test } from './_fixture'; |
|||
|
|||
test.beforeEach(async ({ page, appName }) => { |
|||
await page.goto(`/app/${appName}`); |
|||
}); |
|||
|
|||
test('create rule', async ({ page }) => { |
|||
const ruleName = await createRandomRule(page); |
|||
const ruleCard = page.locator('div.card', { hasText: new RegExp(escapeRegex(ruleName)) }); |
|||
|
|||
await expect(ruleCard).toBeVisible(); |
|||
}); |
|||
|
|||
test('delete rule', async ({ dropdown, page }) => { |
|||
const ruleName = await createRandomRule(page); |
|||
const ruleCard = page.locator('div.card', { hasText: new RegExp(escapeRegex(ruleName)) }); |
|||
|
|||
await ruleCard.getByTestId('options').click(); |
|||
await dropdown.delete(); |
|||
|
|||
await expect(ruleCard).not.toBeVisible(); |
|||
}); |
|||
|
|||
test('disable rule', async ({ dropdown, page }) => { |
|||
const ruleName = await createRandomRule(page); |
|||
const ruleCard = page.locator('div.card', { hasText: new RegExp(escapeRegex(ruleName)) }); |
|||
|
|||
await ruleCard.getByTestId('options').click(); |
|||
await dropdown.action('Disable'); |
|||
|
|||
await expect(ruleCard.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'unchecked'); |
|||
}); |
|||
|
|||
test('enable rule', async ({ dropdown, page }) => { |
|||
const ruleName = await createRandomRule(page); |
|||
const ruleCard = page.locator('div.card', { hasText: new RegExp(escapeRegex(ruleName)) }); |
|||
|
|||
const disableRequest = page.waitForResponse(/rules/); |
|||
|
|||
await ruleCard.getByTestId('options').click(); |
|||
await dropdown.action('Disable'); |
|||
|
|||
await disableRequest; |
|||
|
|||
await ruleCard.getByTestId('options').click(); |
|||
await dropdown.action('Enable'); |
|||
|
|||
await expect(ruleCard.locator('sqx-toggle .toggle-container')).toHaveAttribute('data-state', 'checked'); |
|||
}); |
|||
|
|||
test('edit rule', async ({ dropdown, page }) => { |
|||
const ruleName = await createRandomRule(page); |
|||
const ruleCard = page.locator('div.card', { hasText: new RegExp(escapeRegex(ruleName)) }); |
|||
|
|||
await ruleCard.getByTestId('options').click(); |
|||
await dropdown.action('Edit'); |
|||
|
|||
await expect(page.getByText('Enabled')).toBeVisible(); |
|||
}); |
|||
|
|||
async function createRandomRule(page: Page) { |
|||
const ruleName = `rule-${getRandomId()}`; |
|||
|
|||
await page.getByTestId('rules').click(); |
|||
await page.getByRole('link', { name: /New Rule/ }).click(); |
|||
|
|||
// Setup rule action
|
|||
await page.getByText('Content changed').click(); |
|||
|
|||
// Setup rule trigger
|
|||
await page.getByText('Webhook').click(); |
|||
await page.locator('sqx-formattable-input').first().getByRole('textbox').fill('https:/squidex.io'); |
|||
|
|||
await page.getByRole('button', { name: 'Save' }).click(); |
|||
|
|||
await page.getByText('Enabled').waitFor({ state: 'visible' }); |
|||
|
|||
// Go back
|
|||
await page.getByTestId('back').click(); |
|||
|
|||
// Setup name.
|
|||
await page.locator('div.card', { hasText: /Unnamed Rule/ }).getByRole('heading').first().dblclick(); |
|||
await page.locator('form').getByRole('textbox').fill(ruleName); |
|||
await page.locator('form').getByTestId('save').click(); |
|||
|
|||
return ruleName; |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Page } from '@playwright/test'; |
|||
import { escapeRegex, getRandomId } from '../utils'; |
|||
import { expect, test } from './_fixture'; |
|||
|
|||
test.beforeEach(async ({ page, appName }) => { |
|||
await page.goto(`/app/${appName}`); |
|||
}); |
|||
|
|||
test('create schema', async ({ page }) => { |
|||
const schemaName = await createRandomSchema(page); |
|||
const schemaLink = page.locator('a.nav-link', { hasText: new RegExp(escapeRegex(schemaName)) }); |
|||
|
|||
await expect(schemaLink).toBeVisible(); |
|||
}); |
|||
|
|||
test('delete schema', async ({ dropdown, page }) => { |
|||
const schemaName = await createRandomSchema(page); |
|||
const schemaLink = page.locator('a.nav-link', { hasText: new RegExp(escapeRegex(schemaName)) }); |
|||
|
|||
await page.getByTestId('options').click(); |
|||
await dropdown.delete(); |
|||
|
|||
await expect(schemaLink).not.toBeVisible(); |
|||
}); |
|||
|
|||
test('publish schema', async ({ page }) => { |
|||
await createRandomSchema(page); |
|||
|
|||
await page.getByRole('button', { name: 'Published', exact: true }).click(); |
|||
|
|||
await expect(page.getByRole('button', { name: 'Published', exact: true })).toBeDisabled(); |
|||
}); |
|||
|
|||
test('add field', async ({ page }) => { |
|||
await createRandomSchema(page); |
|||
|
|||
const fieldName = await createRandomField(page); |
|||
const fieldRow = page.getByText(fieldName); |
|||
|
|||
await expect(fieldRow).toBeVisible(); |
|||
}); |
|||
|
|||
test('delete field', async ({ dropdown, page }) => { |
|||
await createRandomSchema(page); |
|||
|
|||
const fieldName = await createRandomField(page); |
|||
const fieldRow = page.locator('div.table-items-row-summary', { hasText: new RegExp(escapeRegex(fieldName)) }); |
|||
|
|||
await fieldRow.getByTestId('options').click(); |
|||
await dropdown.delete(); |
|||
|
|||
await expect(fieldRow).not.toBeVisible(); |
|||
}); |
|||
|
|||
async function createRandomField(page: Page) { |
|||
const fieldName = `field-${getRandomId()}`; |
|||
|
|||
await page.locator('button').filter({ hasText: /^Add Field$/ }).click(); |
|||
|
|||
// Setup name.
|
|||
await page.getByPlaceholder('Enter field name').fill(fieldName); |
|||
|
|||
// Save
|
|||
await page.getByRole('button', { name: 'Create and close' }).click(); |
|||
|
|||
return fieldName; |
|||
} |
|||
|
|||
async function createRandomSchema(page: Page) { |
|||
const schemaName = `schema-${getRandomId()}`; |
|||
|
|||
await page.getByTestId('schemas').click(); |
|||
|
|||
await page.getByTestId('new-schema').click(); |
|||
|
|||
// Setup name.
|
|||
await page.getByLabel('Name (required)').fill(schemaName); |
|||
|
|||
// Save
|
|||
await page.getByRole('button', { name: 'Create' }).click(); |
|||
|
|||
return schemaName; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { test as base } from '../_fixture'; |
|||
|
|||
type LoginFixture = { |
|||
userEmail: string; |
|||
userPassword: string; |
|||
}; |
|||
|
|||
export const test = base.extend<LoginFixture>({ |
|||
userEmail: [ |
|||
'hello@squidex.io', |
|||
{ option: true }, |
|||
], |
|||
userPassword: [ |
|||
'1q2w3e$R', |
|||
{ option: true }, |
|||
], |
|||
}); |
|||
|
|||
export { expect } from '@playwright/test'; |
|||
|
|||
@ -0,0 +1,31 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { STORAGE_STATE } from '../../playwright.config'; |
|||
import { test as setup } from './_fixture'; |
|||
|
|||
setup('prepare login', async ({ page, userEmail, userPassword }) => { |
|||
await page.goto('/'); |
|||
|
|||
// Start waiting for popup before clicking.
|
|||
const popupPromise = page.waitForEvent('popup'); |
|||
|
|||
await page.getByTestId('login').click(); |
|||
|
|||
const popup = await popupPromise; |
|||
await popup.waitForLoadState(); |
|||
|
|||
await popup.getByTestId('login-button').waitFor(); |
|||
|
|||
await popup.getByPlaceholder('Enter Email').fill(userEmail); |
|||
await popup.getByPlaceholder('Enter Password').fill(userPassword); |
|||
await popup.getByTestId('login-button').click(); |
|||
|
|||
await page.waitForURL(/app/); |
|||
|
|||
await page.context().storageState({ path: STORAGE_STATE }); |
|||
}); |
|||
@ -0,0 +1,14 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { expect, test } from '@playwright/test'; |
|||
|
|||
test('has title', async ({ page }) => { |
|||
await page.goto('/app'); |
|||
|
|||
await expect(page).toHaveTitle(/Apps/); |
|||
}); |
|||
@ -0,0 +1,24 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { expect, test } from '@playwright/test'; |
|||
import { getRandomId } from '../utils'; |
|||
|
|||
test('should create app', async ({ page }) => { |
|||
const appName = `my-app-${getRandomId()}`; |
|||
|
|||
await page.goto('/app'); |
|||
|
|||
await page.getByTestId('new-app').click(); |
|||
|
|||
await page.locator('#name').fill(appName); |
|||
await page.getByRole('button', { name: 'Create' }).click(); |
|||
|
|||
const newApp = page.getByRole('heading', { name: appName }); |
|||
|
|||
await expect(newApp).toBeVisible(); |
|||
}); |
|||
@ -0,0 +1,42 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { expect, test } from '@playwright/test'; |
|||
|
|||
test('login', async ({ page }) => { |
|||
await page.goto('/'); |
|||
|
|||
// Start waiting for popup before clicking.
|
|||
const popupPromise = page.waitForEvent('popup'); |
|||
|
|||
await page.getByTestId('login').click(); |
|||
|
|||
const popup = await popupPromise; |
|||
await popup.waitForLoadState(); |
|||
|
|||
await popup.getByTestId('login-button').waitFor(); |
|||
|
|||
await popup.getByPlaceholder('Enter Email').fill('hello@squidex.io'); |
|||
await popup.getByPlaceholder('Enter Password').fill('1q2w3e$R'); |
|||
await popup.getByTestId('login-button').click(); |
|||
|
|||
await page.waitForURL(/app/); |
|||
|
|||
await expect(page).toHaveTitle(/Apps/); |
|||
}); |
|||
|
|||
test('visual test', async ({ page }) => { |
|||
await page.goto('/'); |
|||
|
|||
// Start waiting for popup before clicking.
|
|||
const popupPromise = page.waitForEvent('popup'); |
|||
|
|||
await page.getByTestId('login').click(); |
|||
|
|||
const popup = await popupPromise; |
|||
await expect(popup).toHaveScreenshot({ fullPage: true }); |
|||
}); |
|||
@ -0,0 +1,18 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { expect, test } from '@playwright/test'; |
|||
|
|||
test('has title', async ({ page }) => { |
|||
await page.goto('/'); |
|||
await expect(page).toHaveTitle(/Squidex/); |
|||
}); |
|||
|
|||
test('visual test', async ({ page }) => { |
|||
await page.goto('/'); |
|||
await expect(page).toHaveScreenshot(); |
|||
}); |
|||
@ -0,0 +1,48 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import fs from 'fs/promises'; |
|||
import path from 'path'; |
|||
import { TEMPORARY_PATH } from '../playwright.config'; |
|||
|
|||
export function getRandomId() { |
|||
const result = new Date().getTime().toString(); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
export function escapeRegex(string: string) { |
|||
const result = string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
export async function writeJsonAsync(name: string, json: any) { |
|||
const fullPath = await getPath(name); |
|||
|
|||
await fs.writeFile(fullPath, JSON.stringify(json), { encoding: 'utf8' }); |
|||
} |
|||
|
|||
export async function readJsonAsync<T>(name: string, defaultValue: T) { |
|||
const fullPath = await getPath(name); |
|||
|
|||
const json = await fs.readFile(fullPath, 'utf8'); |
|||
|
|||
if (json) { |
|||
return JSON.parse(json); |
|||
} else { |
|||
return defaultValue; |
|||
} |
|||
} |
|||
|
|||
async function getPath(name: string) { |
|||
const fullPath = path.join(TEMPORARY_PATH, `${name}.json`); |
|||
|
|||
await fs.mkdir(TEMPORARY_PATH, { recursive: true }); |
|||
|
|||
return fullPath; |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
/* Visit https://aka.ms/tsconfig to read more about this file */ |
|||
|
|||
/* Projects */ |
|||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ |
|||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ |
|||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ |
|||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ |
|||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ |
|||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ |
|||
|
|||
/* Language and Environment */ |
|||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ |
|||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ |
|||
// "jsx": "preserve", /* Specify what JSX code is generated. */ |
|||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ |
|||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ |
|||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ |
|||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ |
|||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ |
|||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ |
|||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ |
|||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ |
|||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ |
|||
|
|||
/* Modules */ |
|||
"module": "commonjs", /* Specify what module code is generated. */ |
|||
// "rootDir": "./", /* Specify the root folder within your source files. */ |
|||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ |
|||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ |
|||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ |
|||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ |
|||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ |
|||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */ |
|||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ |
|||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ |
|||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ |
|||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ |
|||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ |
|||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ |
|||
// "resolveJsonModule": true, /* Enable importing .json files. */ |
|||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ |
|||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ |
|||
|
|||
/* JavaScript Support */ |
|||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ |
|||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ |
|||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ |
|||
|
|||
/* Emit */ |
|||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ |
|||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ |
|||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ |
|||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ |
|||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ |
|||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ |
|||
// "outDir": "./", /* Specify an output folder for all emitted files. */ |
|||
// "removeComments": true, /* Disable emitting comments. */ |
|||
// "noEmit": true, /* Disable emitting files from a compilation. */ |
|||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ |
|||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ |
|||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ |
|||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ |
|||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ |
|||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ |
|||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ |
|||
// "newLine": "crlf", /* Set the newline character for emitting files. */ |
|||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ |
|||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ |
|||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ |
|||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ |
|||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ |
|||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ |
|||
|
|||
/* Interop Constraints */ |
|||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ |
|||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ |
|||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ |
|||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ |
|||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ |
|||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ |
|||
|
|||
/* Type Checking */ |
|||
"strict": true, /* Enable all strict type-checking options. */ |
|||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ |
|||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ |
|||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ |
|||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ |
|||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ |
|||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ |
|||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ |
|||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ |
|||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ |
|||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ |
|||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ |
|||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ |
|||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ |
|||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ |
|||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ |
|||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ |
|||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ |
|||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ |
|||
|
|||
/* Completeness */ |
|||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ |
|||
"skipLibCheck": true /* Skip type checking all .d.ts files. */ |
|||
} |
|||
} |
|||
Loading…
Reference in new issue