diff --git a/.gitea/workflows/main.yml b/.gitea/workflows/main.yml index 9678fb55..af227b9a 100644 --- a/.gitea/workflows/main.yml +++ b/.gitea/workflows/main.yml @@ -36,6 +36,7 @@ jobs: run: npm run e2e env: BASE_URL: ${{ github.event.deployment_status.target_url }} + DEBUG: pw:api email-templates: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 5e15e32c..34f1e4f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.devcontainer dist/ node_modules/ npm-debug.log* @@ -22,6 +23,8 @@ bun.lockb /blob-report/ /playwright/.cache/ /plawright-report/ +target +.github/dependabot.yml .output .vinxi diff --git a/README.md b/README.md index fa758bd2..81599367 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Use Bun to manage packages. bun i ``` -## Useful commands -run checks +## Useful commands +run checks ``` bun run typecheck ``` @@ -15,3 +15,34 @@ fix styles, imports, formatting and autofixable linting errors: ``` bun run fix ``` + +## Config of variables + +- All vars are already in place and wroted in + ``` + /src/utils/config.ts + ``` + +# End-to-End (E2E) Tests + +This directory contains end-to-end tests. These tests are written using [Playwright](https://playwright.dev/) + +## Structure + +- `/tests/*`: This directory contains the test files. +- `/playwright.config.ts`: This is the configuration file for Playwright. + +## Getting Started + +Follow these steps: + +1. **Install dependencies**: Run `pnpm e2e:install` to install the necessary dependencies for running the tests. + +2. **Run the tests**: After using `pnpm e2e:tests`. + +## Additional Information + +If workers is no needed use: +- `npx playwright test --project=webkit --workers 4` + +For more information on how to write tests using Playwright - [Playwright documentation](https://playwright.dev/docs/intro). diff --git a/package.json b/package.json index 72cb8757..e2c4af09 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "build": "vinxi build", "start": "vinxi start", "codegen": "graphql-codegen", - "e2e": "npx playwright test --project=webkit", - "fix": "npx @biomejs/biome check src/. --write && stylelint **/*.{scss,css} --fix", + "e2e:tests": "npx playwright test --project=webkit", + "e2e:install": "npx playwright install webkit && npx playwright install-deps ", + "fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix", "format": "npx @biomejs/biome format src/. --write", "postinstall": "npm run codegen && npx patch-package", "typecheck": "tsc --noEmit" @@ -117,6 +118,9 @@ "idb": "^8.0.0", "mailgun.js": "^10.2.1" }, + "trustedDependencies": [ + "@biomejs/biome" + ], "engines": { "node": "20.x" } diff --git a/playwright.config.ts b/playwright.config.ts index 978dadb9..5cae497a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,9 +10,10 @@ import { defineConfig, devices } from '@playwright/test' * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ + /* Directory to search for tests */ testDir: './tests', /* Run tests in files in parallel */ - fullyParallel: true, + fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ @@ -20,27 +21,23 @@ export default defineConfig({ /* 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', + reporter: 'list', + /* Timeout for each test */ + timeout: 40000, /* 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: 'http://127.0.0.1:3000', - + baseURL: 'https://localhost:3000', + /* Headless */ + headless: true, + /* Ignode SSL certificates */ + ignoreHTTPSErrors: true, /* 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: 'chromium', - use: { ...devices['Desktop Chrome'] } - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] } - }, { name: 'webkit', @@ -69,9 +66,11 @@ export default defineConfig({ ] /* Run local dev server before starting the tests */ - //webServer: { - // command: 'npm run dev', - // url: 'https://localhost:3000', - // reuseExistingServer: !process.env.CI, - //}, + webServer: { + command: 'npm run dev', + url: 'https://localhost:3000', + ignoreHTTPSErrors: true, + reuseExistingServer: !process.env.CI, + timeout: 5 * 60 * 1000, + }, }) diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/graphql/client/core.ts @@ -0,0 +1 @@ + diff --git a/tests/auth-drafts-actions.spec.ts b/tests/auth-drafts-actions.spec.ts new file mode 100644 index 00000000..f060d748 --- /dev/null +++ b/tests/auth-drafts-actions.spec.ts @@ -0,0 +1,128 @@ +import { test, expect, type Page } from '@playwright/test'; +import https from 'https'; + + +/* Global starting test config */ + +let page: Page; + +function httpsGet(url: string): Promise { + return new Promise((resolve, reject) => { + https.get(url, { + rejectUnauthorized: false // Ignore SSL certificate errors + }, (res) => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Request failed with status code ${res.statusCode}`)); + } + }).on('error', (error) => { + reject(error); + }); + }); +} +async function waitForServer(url: string, timeout: number = 150000) { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + await httpsGet(url); + return true; + } catch (error) { + // Ignore errors and try again + console.log (`Error fetching ${url}: ${error.message}`); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + throw new Error(`Server at ${url} did not start within ${timeout} ms`); +} +test.beforeAll(async ({ browser }) => { + console.log('Waiting for the server to start...'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + const baseURL = 'https://localhost:3000'; + await waitForServer(baseURL); + page = await browser.newPage(); + test.setTimeout(150000); + await page.goto(baseURL); + await expect(page).toHaveTitle(/Дискурс/); + console.log('Localhost server started successfully!'); + await page.close(); + }); +test.afterAll(async () => { + await page.close(); + }); + + +/* TESTS section */ + +test.beforeEach(async ({ page }) => { + await page.goto(`/`); + test.setTimeout(80000); + await page.getByRole('link', { name: 'Войти' }).click(); + await page.getByPlaceholder('Почта').click(); + await page.getByPlaceholder('Почта').fill('guests@discours.io'); + await page.getByPlaceholder('Пароль').click(); + await page.getByPlaceholder('Пароль').fill('Gue$tP@ss'); + await page.getByRole('button', { name: 'Войти' }).click(); +}); + +test.describe('*****Undone***** Drafts - article', () => { + test('Open /create', async ({ page }) => { + await page.goto(`/create`); + await expect(page).toHaveTitle('Выберите тип публикации'); + }); +}); + +/* test('Create article', async ({ page }) => { + await page.goto(`/create`); + await page.locator('li').filter({ hasText: 'статья' }).locator('img').click(); +}); + +test('Check Draft', async ({ page }) => { + +}); */ + +/* test('Drafts - create literature', async ({ page }) => { + await page.getByRole('button', { name: 'Т.Р' }).click(); + await page.getByRole('link', { name: 'Черновики' }).click(); + await page.getByRole('link', { name: 'Создать публикацию' }).click(); + await page.locator('li').filter({ hasText: /^литература$/ }).locator('img').click(); + Fill the form + Save + Check is it created +}); */ + +/* test('Drafts - create images', async ({ page }) => { + await page.getByRole('button', { name: 'Т.Р' }).click(); + await page.getByRole('link', { name: 'Черновики' }).click(); + await page.getByRole('link', { name: 'Создать публикацию' }).click(); + await page.locator('li').filter({ hasText: 'изображения' }).locator('img').click(); + Fill the form + Save + Check is it created +}); */ + +/* test('Drafts - create music', async ({ page }) => { + await page.getByRole('button', { name: 'Т.Р' }).click(); + await page.getByRole('link', { name: 'Черновики' }).click(); + await page.getByRole('link', { name: 'Создать публикацию' }).click(); + await page.locator('li').filter({ hasText: 'музыка' }).locator('img').click(); + Fill the form + Save + Check is it created +}); */ + +/* test('Drafts - create video', async ({ page }) => { + await page.getByRole('button', { name: 'Т.Р' }).click(); + await page.getByRole('link', { name: 'Черновики' }).click(); + await page.getByRole('link', { name: 'Создать публикацию' }).click(); + await page.locator('li').filter({ hasText: 'видео' }).locator('img').click(); + Fill the form + Save + Check is it created +}); */ + + +/* test('Post topic', async ({ page }) => { + Open Draft + Post +});*/ \ No newline at end of file diff --git a/tests/auth-topics-actions.spec.ts b/tests/auth-topics-actions.spec.ts new file mode 100644 index 00000000..4e4ad070 --- /dev/null +++ b/tests/auth-topics-actions.spec.ts @@ -0,0 +1,115 @@ +import { test, expect, type Page } from '@playwright/test'; +import https from 'https'; + + +/* Global starting test config */ + +let page: Page; + +function httpsGet(url: string): Promise { + return new Promise((resolve, reject) => { + https.get(url, { + rejectUnauthorized: false // Ignore SSL certificate errors + }, (res) => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Request failed with status code ${res.statusCode}`)); + } + }).on('error', (error) => { + reject(error); + }); + }); +} +async function waitForServer(url: string, timeout: number = 150000) { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + await httpsGet(url); + return true; + } catch (error) { + // Ignore errors and try again + console.log (`Error fetching ${url}: ${error.message}`); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + throw new Error(`Server at ${url} did not start within ${timeout} ms`); +} +test.beforeAll(async ({ browser }) => { + console.log('Waiting for the server to start...'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + const baseURL = 'https://localhost:3000'; + await waitForServer(baseURL); + page = await browser.newPage(); + test.setTimeout(150000); + await page.goto(baseURL); + await expect(page).toHaveTitle(/Дискурс/); + await page.getByRole('link', { name: 'Войти' }).click(); + console.log('Localhost server started successfully!'); + await page.close(); + }); +test.afterAll(async () => { + await page.close(); + }); + + +/* TESTS section */ + +/* Random Generator */ +function generateRandomString(length = 10) { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +test.beforeEach(async ({ page }) => { + await page.goto(`/`); + /* test.setTimeout(80000); */ + await page.getByRole('link', { name: 'Войти' }).click(); + await page.getByPlaceholder('Почта').click(); + await page.getByPlaceholder('Почта').fill('guests@discours.io'); + await page.getByPlaceholder('Пароль').click(); + await page.getByPlaceholder('Пароль').fill('Gue$tP@ss'); + await page.getByRole('button', { name: 'Войти' }).click(); +}); + +test.describe('Topic Actions', () => { + test('Follow topic', async ({ page }) => { + await page.getByRole('link', { name: 'темы', exact: true }).click(); + await page.getByRole('link', { name: 'Общество Статьи о политике, экономике и обществе, об актуальных событиях, людях, мнениях. Тексты про историю и современность, про то, что происходит в России и мире' }).click(); + await page.getByRole('button', { name: 'Подписаться на тему' }).click(); + await expect(page.getByRole('button', { name: 'Отписаться от темы' })).toBeVisible(); + }); + test('Unfollow topic', async ({ page }) => { + await page.getByRole('link', { name: 'темы', exact: true }).click(); + await page.getByRole('link', { name: 'Общество Статьи о политике, экономике и обществе, об актуальных событиях, людях, мнениях. Тексты про историю и современность, про то, что происходит в России и мире' }).click(); + await page.getByRole('button', { name: 'Отписаться от темы' }).click(); + await expect(page.getByRole('button', { name: 'Подписаться на тему' })).toBeVisible(); + }); + test('Add comment to topic', async ({ page }) => { + const randomString = generateRandomString(); + const currentDate = new Date(); + await page.getByRole('button', { name: 'Т.Р' }).click(); + await page.getByRole('link', { name: 'Профиль' }).click(); + await page.getByRole('link', { name: 'Тестируем функционал' }).first().click(); + await page.locator('.tiptap').click(); + await page.locator('.tiptap').fill('Проверка Комментариев: ' + randomString + ' ' + currentDate); + await page.getByRole('button', { name: 'Отправить' }).click(); + await expect(page.getByText('Проверка Комментариев: ' + randomString + ' ' + currentDate)).toBeVisible(); + }); + test('Edit comment to topic', async ({ page }) => { + await page.getByRole('button', { name: 'Т.Р' }).click(); + await page.getByRole('link', { name: 'Комментарии' }).click(); + await page.locator('[id^="comment_"]').filter({ hasText: 'Проверка Комментариев' }).first().hover(); + await page.getByRole('button', { name: 'Редактировать', exact: true }).first().click(); + const randomString = generateRandomString(); + const currentDate = new Date(); + await page.locator('.tiptap').fill('Редактируемый Комментарий: ' + randomString + ' ' + currentDate); + await page.getByRole('button', { name: 'Сохранить' }).click(); + await expect(page.getByText('Редактируемый Комментарий: ' + randomString + ' ' + currentDate)).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/auth-user-actions.spec.ts b/tests/auth-user-actions.spec.ts new file mode 100644 index 00000000..cf2b157b --- /dev/null +++ b/tests/auth-user-actions.spec.ts @@ -0,0 +1,111 @@ +import { test, expect, type Page } from '@playwright/test'; +import https from 'https'; + +let context: any; +let page: Page; + +/* Global starting test config */ + +function httpsGet(url: string): Promise { + return new Promise((resolve, reject) => { + https.get(url, { + rejectUnauthorized: false // Ignore SSL certificate errors + }, (res) => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Request failed with status code ${res.statusCode}`)); + } + }).on('error', (error) => { + reject(error); + }); + }); +} +async function waitForServer(url: string, timeout: number = 150000) { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + await httpsGet(url); + return true; + } catch (error) { + // Ignore errors and try again + console.log (`Error fetching ${url}: ${error.message}`); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + throw new Error(`Server at ${url} did not start within ${timeout} ms`); +} +test.beforeAll(async ({ browser }) => { + console.log('Waiting for the server to start...'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + const baseURL = 'https://localhost:3000'; + await waitForServer(baseURL); + context = await browser.newContext(); + page = await context.newPage(); + test.setTimeout(150000); + await page.goto(baseURL); + await expect(page).toHaveTitle(/Дискурс/); + await page.getByRole('link', { name: 'Войти' }).click(); + console.log('Localhost server started successfully!'); + await page.close(); + }); + +/* TESTS section */ + +/* Random Generator */ +function generateRandomString(length = 10) { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +test.beforeEach(async ({page}) => { + await page.goto(`/`); + /* test.setTimeout(80000); */ + await page.getByRole('link', { name: 'Войти' }).click(); + await page.getByPlaceholder('Почта').click(); + await page.getByPlaceholder('Почта').fill('guests@discours.io'); + await page.getByPlaceholder('Пароль').click(); + await page.getByPlaceholder('Пароль').fill('Gue$tP@ss'); + await page.getByRole('button', { name: 'Войти' }).click(); +}); + +test.describe('User Actions', () => { + test('User sandwitch menu', async ({page}) => { + await page.getByRole('button', { name: 'Т.Р' }).click(); + await expect(page.getByRole('link', { name: 'Профиль' })).toBeVisible(); + await page.getByRole('button', { name: 'Т.Р' }).click(); + }); + test('Follow user', async ({page}) => { + await page.getByRole('link', { name: 'авторы', exact: true }).click(); + await page.getByRole('link', { name: 'Дискурс На сайте c 16 июня' }).click(); + await page.getByRole('button', { name: 'Подписаться' }).click(); + await expect(page.getByRole('main').getByRole('button', { name: 'Вы подписаны' })).toBeVisible(); + }); + test('Unfollow user', async ({page}) => { + await page.getByRole('link', { name: 'авторы', exact: true }).click(); + await page.getByRole('link', { name: 'Дискурс На сайте c 16 июня' }).click(); + await page.getByRole('button', { name: 'Вы подписаны' }).click(); + await expect(page.getByRole('main').getByRole('button', { name: 'Подписаться' })).toBeVisible(); + }); + test('Change user data', async ({page}) => { + await page.getByRole('button', { name: 'Т.Р' }).click(); + await page.getByRole('link', { name: 'Профиль' }).click(); + await page.getByRole('button', { name: 'Редактировать профиль' }).click(); + await page.locator('.tiptap').click(); + const randomString = generateRandomString(); + const currentDate = new Date(); + await page.locator('.tiptap').fill('test: ' + randomString + ' ' + currentDate); + try { + const button = await page.getByRole('button', { name: 'Сохранить настройки' }); + await button.click(); + } catch (error) { + console.log('Button has disappeared'); + } + await expect(page.getByText('test: ' + randomString + ' ' + currentDate)).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/tests/page-sections.spec.ts b/tests/page-sections.spec.ts new file mode 100644 index 00000000..7ee6aeb6 --- /dev/null +++ b/tests/page-sections.spec.ts @@ -0,0 +1,71 @@ +import { test, expect, type Page } from '@playwright/test'; +import https from 'https'; + +/* Global starting test config */ + +let page: Page; + +function httpsGet(url: string): Promise { + return new Promise((resolve, reject) => { + https.get(url, { + rejectUnauthorized: false // Ignore SSL certificate errors + }, (res) => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Request failed with status code ${res.statusCode}`)); + } + }).on('error', (error) => { + reject(error); + }); + }); +} +async function waitForServer(url: string, timeout: number = 150000) { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + await httpsGet(url); + return true; + } catch (error) { + // Ignore errors and try again + console.log (`Error fetching ${url}: ${error.message}`); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + throw new Error(`Server at ${url} did not start within ${timeout} ms`); +} +test.beforeAll(async ({ browser }) => { + console.log('Waiting for the server to start...'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + const baseURL = 'https://localhost:3000'; + await waitForServer(baseURL); + page = await browser.newPage(); + test.setTimeout(150000); + await page.goto(baseURL); + await expect(page).toHaveTitle(/Дискурс/); + console.log('Localhost server started successfully!'); + }); +test.afterAll(async () => { + await page.close(); + }); + + +/* TESTS section */ + +const pagesTitles = { + '/': /Дискурс/, + '/feed': /Лента/, + '/about/help': /Поддержите Дискурс/, + '/authors': /Авторы/, + '/topics': /Темы и сюжеты/, + '/inbox': /Входящие/, +} +test.describe('Pages open', () => { + Object.keys(pagesTitles).forEach((res: string) => { + test(`Open Page ${res}`, async ({ page }) => { + await page.goto(`${res}`) + const title = pagesTitles[res] + await expect(page).toHaveTitle(title) + }) + }) +}); \ No newline at end of file