тесты для таймера

This commit is contained in:
Nastya 2025-10-02 03:07:05 +03:00
parent 4ff5e42d2c
commit 4c300528fa
17 changed files with 1628 additions and 383 deletions

@ -2,7 +2,7 @@ import { defineConfig } from "cypress";
export default defineConfig({ export default defineConfig({
e2e: { e2e: {
baseUrl: "http://localhost:3000", baseUrl: "http://localhost:5173",
viewportWidth: 1440, viewportWidth: 1440,
viewportHeight: 900, viewportHeight: 900,
supportFile: false, supportFile: false,

6
cypress/README Normal file

@ -0,0 +1,6 @@
Аккаунт с квизами для тестирования
testakk@mail.ru
testakktestakk
3ba7deeb-9bb6-4057-a5a3-798e935958a0 - БС, 3 варианты ответов, таймер 3 сек, quiz линейный BC3VT3SQL
e69d4d52-50f6-4c2a-8fb6-cf92adaf6ca0 - БС, 3 варианты ответов, таймер 3 сек, quiz ветвящийся BC3VT3SQВ

@ -0,0 +1,37 @@
describe("БС, 3 варианты ответов, таймер 3 сек, quiz ветвящийся BC3VT3SQB", () => {
it("необходимо завершить тест тремя вопросами, проверить отсутствие заголовка 'не ветвящийся' и проверить, отключена ли кнопка назад", () => {
cy.visit("/e69d4d52-50f6-4c2a-8fb6-cf92adaf6ca0");
cy.get("body").should("be.visible");
// Проверяем первый вопрос
cy.get("#test-question-title", { timeout: 10000 }).should("be.visible");
cy.get("#test-question-title").should("contain", "первый вопрос ветвления");
cy.get("#test-prev-button").should("be.disabled");
// Проверяем второй вопрос
cy.get("#test-question-title").should("be.visible");
cy.get("#test-question-title").should("contain", "второй вопрос ветвления");
cy.get("#test-prev-button").should("be.disabled");
// Проверяем третий вопрос
cy.get("#test-question-title").should("be.visible");
cy.get("#test-question-title").should("contain", "третий вопрос ветвления");
cy.get("#test-prev-button").should("be.disabled");
// Проверяем открытие страницы с контактной формой
cy.get("#test-contact-form").should("be.visible");
// Заполняем контактную форму
cy.get('input[placeholder*="Имя"], input[placeholder*="Name"]').type("Тестовое имя");
cy.get('input[placeholder*="Email"], input[placeholder*="Почта"]').type("test@example.com");
// Соглашаемся с условиями
cy.get('input[type="checkbox"]').check();
// Нажимаем кнопку получения результатов
cy.get("button").contains("Получить результаты").click();
// Проверяем открытие страницы с результатами
cy.get("#test-result-form").should("be.visible");
});
});

@ -0,0 +1,36 @@
describe("БС, 3 варианты ответов, таймер 3 сек, quiz ветвящийся BC3VT3SQL", () => {
it("необходимо завершить тест тремя вопросами и проверить, отключена ли кнопка назад", () => {
cy.visit("/3ba7deeb-9bb6-4057-a5a3-798e935958a0");
cy.get("body").should("be.visible");
// Проверяем первый вопрос
cy.get("#test-question-title", { timeout: 10000 }).should("be.visible");
cy.get("#test-question-title").should("contain", "первый вопрос");
cy.get("#test-prev-button").should("be.disabled");
// Проверяем второй вопрос
cy.get("#test-question-title").should("be.visible");
cy.get("#test-question-title").should("contain", "второй вопрос");
cy.get("#test-prev-button").should("be.disabled");
// Проверяем третий вопрос
cy.get("#test-question-title").should("be.visible");
cy.get("#test-question-title").should("contain", "третий вопрос");
cy.get("#test-prev-button").should("be.disabled");
// Проверяем открытие страницы с контактной формой
cy.get("#test-contact-form").should("be.visible");
// Заполняем контактную форму
cy.get('input[placeholder*="Email"], input[placeholder*="Почта"]').type("test@example.com");
// Соглашаемся с условиями
cy.get('input[type="checkbox"]').check();
// Нажимаем кнопку получения результатов
cy.get("button").contains("Получить результаты").click();
// Проверяем открытие страницы с результатами
cy.get("#test-result-form").should("be.visible");
});
});

@ -0,0 +1,248 @@
/// <reference types="cypress" />
import "../support/commands";
describe("Проверка apology page", () => {
// Конфигурация тестов для разных языков
const localeConfigs = [
{
locale: "ru",
urlPrefix: "", // русский по умолчанию
description: "русский (по умолчанию)",
},
{
locale: "ru",
urlPrefix: "/ru",
description: "русский (явно указан)",
},
{
locale: "en",
urlPrefix: "/en",
description: "английский",
},
{
locale: "uz",
urlPrefix: "/uz",
description: "узбекский",
},
];
// Тестовые случаи с ожидаемыми ошибками
const testCases = [
{
name: "неопубликованный квиз",
path: "/b0d3c6d1-49b8-4cf8-bb4c-bde05eebb659",
expectedError: "quiz is inactive",
},
{
name: "опубликованный квиз без вопросов",
path: "/228df092-1f9b-4c04-b08d-08726f0ef223",
expectedError: "no questions found",
},
{
name: "пустой qid",
path: "/",
expectedError: "no quiz id",
},
{
name: "некорректный qid",
path: "/invalid-quiz-id-12345",
expectedError: "invalid request data",
},
{
name: "несуществующий qid",
path: "/00000000-0000-0000-0000-000000000000",
expectedError: "invalid request data",
},
{
name: "qid с неправильным форматом",
path: "/not-a-valid-uuid",
expectedError: "invalid request data",
},
];
// Базовые тесты без проверки текста (для всех случаев)
describe("Базовые проверки отображения apology page", () => {
it("необходимо проверить, что apology page отображает свои ошибки", () => {
cy.visit("/apology-page");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для неопубликованного квиза", () => {
cy.visit("/b0d3c6d1-49b8-4cf8-bb4c-bde05eebb659");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для опубликованного квиза без вопросов", () => {
cy.visit("/228df092-1f9b-4c04-b08d-08726f0ef223");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для пустого qid", () => {
cy.visit("/");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для некорректного qid", () => {
cy.visit("/invalid-quiz-id-12345");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для несуществующего qid", () => {
cy.visit("/00000000-0000-0000-0000-000000000000");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с неправильным форматом", () => {
cy.visit("/not-a-valid-uuid");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с пустой строкой", () => {
cy.visit("/ ");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с null значением", () => {
cy.visit("/null");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с undefined значением", () => {
cy.visit("/undefined");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с пустой строкой в URL", () => {
cy.visit("/%20");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с специальными символами", () => {
cy.visit("/!@#$%^&*()");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с очень длинной строкой", () => {
cy.visit("/" + "a".repeat(1000));
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с SQL инъекцией", () => {
cy.visit("/'; DROP TABLE quizzes; --");
cy.get("#test-apology-page").should("be.visible");
});
it("должен показать apology page для qid с XSS попыткой", () => {
cy.visit("/<script>alert('xss')</script>");
cy.get("#test-apology-page").should("be.visible");
});
});
// Параметризованные тесты с проверкой локализации
localeConfigs.forEach(({ locale, urlPrefix, description }) => {
describe(`Локализация: ${description}`, () => {
testCases.forEach(({ name, path, expectedError }) => {
it(`показывает правильную ошибку для "${name}" на ${description}`, () => {
const fullUrl = `${urlPrefix}${path}`;
cy.visit(fullUrl);
cy.get("#test-apology-page").should("be.visible");
// Проверяем что отображается правильный текст ошибки с использованием кешированных переводов
cy.shouldHaveTranslation("#test-apology-page", expectedError, locale);
});
});
// Специальные тесты для разных типов ошибок
it(`показывает ошибку неактивного квиза на ${description}`, () => {
cy.visit(`${urlPrefix}/b0d3c6d1-49b8-4cf8-bb4c-bde05eebb659`);
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "quiz is inactive", locale);
});
it(`показывает ошибку отсутствия вопросов на ${description}`, () => {
cy.visit(`${urlPrefix}/228df092-1f9b-4c04-b08d-08726f0ef223`);
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "no questions found", locale);
});
it(`показывает ошибку отсутствия quiz id на ${description}`, () => {
cy.visit(`${urlPrefix}/`);
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "no quiz id", locale);
});
it(`показывает ошибку невалидных данных на ${description}`, () => {
cy.visit(`${urlPrefix}/invalid-quiz-id-12345`);
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "invalid request data", locale);
});
});
});
// Дополнительные тесты для проверки приоритета локалей
describe("Приоритет выбора локали", () => {
it("использует русскую локаль по умолчанию когда язык не указан", () => {
cy.visit("/b0d3c6d1-49b8-4cf8-bb4c-bde05eebb659");
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "quiz is inactive", "ru");
});
it("использует явно указанную русскую локаль", () => {
cy.visit("/ru/b0d3c6d1-49b8-4cf8-bb4c-bde05eebb659");
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "quiz is inactive", "ru");
});
it("использует английскую локаль когда указан /en", () => {
cy.visit("/en/b0d3c6d1-49b8-4cf8-bb4c-bde05eebb659");
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "quiz is inactive", "en");
});
it("использует узбекскую локаль когда указан /uz", () => {
cy.visit("/uz/b0d3c6d1-49b8-4cf8-bb4c-bde05eebb659");
cy.get("#test-apology-page").should("be.visible");
cy.shouldHaveTranslation("#test-apology-page", "quiz is inactive", "uz");
});
});
// Тесты для проверки доступности файлов переводов
describe("Доступность файлов переводов", () => {
it("файл перевода ru.json доступен и содержит все ключи", () => {
cy.loadTranslations("ru").then((translations) => {
expect(translations).to.have.property("quiz is inactive");
expect(translations).to.have.property("no_questions_found");
expect(translations).to.have.property("quiz is empty");
expect(translations).to.have.property("quiz already completed");
expect(translations).to.have.property("no quiz id");
expect(translations).to.have.property("quiz data is null");
expect(translations).to.have.property("invalid request data");
});
});
it("файл перевода en.json доступен и содержит все ключи", () => {
cy.loadTranslations("en").then((translations) => {
expect(translations).to.have.property("quiz_is_inactive");
expect(translations).to.have.property("no_questions_found");
expect(translations).to.have.property("quiz_is_empty");
expect(translations).to.have.property("quiz_already_completed");
expect(translations).to.have.property("no_quiz_id");
expect(translations).to.have.property("quiz_data_is_null");
expect(translations).to.have.property("invalid_request_data");
});
});
it("файл перевода uz.json доступен и содержит все ключи", () => {
cy.loadTranslations("uz").then((translations) => {
expect(translations).to.have.property("quiz_is_inactive");
expect(translations).to.have.property("no_questions_found");
expect(translations).to.have.property("quiz_is_empty");
expect(translations).to.have.property("quiz_already_completed");
expect(translations).to.have.property("no_quiz_id");
expect(translations).to.have.property("quiz_data_is_null");
expect(translations).to.have.property("invalid_request_data");
});
});
});
});

@ -0,0 +1,72 @@
// Глобальная Map для кеширования переводов
const translationCache = new Map();
/**
* Загружает переводы для указанной локали с кешированием
* Если переводы уже загружены - возвращает из кеша
*/
Cypress.Commands.add("loadTranslations", (locale: string = "ru") => {
// Проверяем есть ли уже переводы в кеше для этой локали
if (translationCache.has(locale)) {
cy.log(`Переводы для локали "${locale}" уже загружены, используем кеш`);
return cy.wrap(translationCache.get(locale));
}
cy.log(`Загружаем переводы для локали: ${locale}`);
// Делаем HTTP запрос к файлу перевода
return cy
.request({
url: `/locales/${locale}.json`,
failOnStatusCode: true,
})
.then((response) => {
// Проверяем что запрос успешен
if (response.status !== 200) {
throw new Error(`Не удалось загрузить переводы для локали ${locale}. Status: ${response.status}`);
}
// Проверяем что ответ содержит данные
if (!response.body || typeof response.body !== "object") {
throw new Error(`Получен некорректный ответ для локали ${locale}`);
}
// Сохраняем переводы в кеш
translationCache.set(locale, response.body);
cy.log(
`Переводы для локали "${locale}" успешно загружены и закешированы. Ключей: ${Object.keys(response.body).length}`
);
return cy.wrap(response.body);
});
});
/**
* Получает конкретный перевод по ключу с использованием кеша
*/
Cypress.Commands.add("getTranslation", (key: string, locale: string = "ru") => {
// Сначала загружаем переводы (использует кеш если уже загружены)
return cy.loadTranslations(locale).then((translations) => {
// Проверяем что ключ существует в переводах
if (!translations.hasOwnProperty(key)) {
const availableKeys = Object.keys(translations).join(", ");
throw new Error(`Ключ перевода "${key}" не найден в локали "${locale}". Доступные ключи: ${availableKeys}`);
}
const translatedText = translations[key];
cy.log(`Перевод для ключа "${key}" в локали "${locale}": "${translatedText}"`);
return cy.wrap(translatedText as unknown as string);
});
});
/**
* Проверяет что элемент содержит правильный перевод
*/
Cypress.Commands.add("shouldHaveTranslation", (selector: string, key: string, locale: string = "ru") => {
// Получаем ожидаемый текст через кешированные переводы
cy.getTranslation(key, locale).then((expectedText) => {
// Проверяем что элемент содержит ожидаемый текст
cy.get(selector).should("contain", expectedText);
});
});

7
cypress/support/index.d.ts vendored Normal file

@ -0,0 +1,7 @@
declare namespace Cypress {
interface Chainable {
loadTranslations(locale?: string): Chainable<any>;
getTranslation(key: string, locale?: string): Chainable<string>;
shouldHaveTranslation(selector: string, key: string, locale?: string): Chainable<void>;
}
}

@ -10,6 +10,7 @@ export const ApologyPage = ({ error }: Props) => {
return ( return (
<Box <Box
id="test-apology-page"
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",

@ -301,6 +301,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
return ( return (
<Box <Box
id="test-contact-form"
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",

@ -123,6 +123,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
}, [resultQuestion]); }, [resultQuestion]);
return ( return (
<Box <Box
id="test-result-form"
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",

@ -79,6 +79,7 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
return ( return (
<Box> <Box>
<Typography <Typography
id="test-question-title"
variant="h5" variant="h5"
color={theme.palette.text.primary} color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }} sx={{ wordBreak: "break-word" }}

@ -23,6 +23,7 @@ export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }:
/> />
) : ( ) : (
<Button <Button
id="test-next-button"
disabled={!isNextButtonEnabled} disabled={!isNextButtonEnabled}
variant="contained" variant="contained"
sx={{ sx={{

@ -17,6 +17,7 @@ export default function PrevButton({ isPreviousButtonEnabled, moveToPrevQuestion
return ( return (
<Button <Button
id="test-prev-button"
disabled={!isPreviousButtonEnabled} disabled={!isPreviousButtonEnabled}
variant="contained" variant="contained"
sx={{ sx={{

@ -36,7 +36,10 @@ export const CustomCircularTimer: React.FC<CircularTimerProps> = ({
const progress = (remaining / duration) * 100; const progress = (remaining / duration) * 100;
return ( return (
<Box sx={{ position: "relative", display: "inline-flex", width: size, height: size }}> <Box
id="test-timer"
sx={{ position: "relative", display: "inline-flex", width: size, height: size }}
>
{/* Серый фон */} {/* Серый фон */}
<Box <Box
sx={{ sx={{

825
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -27,7 +27,8 @@
"cypress:open": "cypress open", "cypress:open": "cypress open",
"prepublishOnly": "npm run build:package", "prepublishOnly": "npm run build:package",
"deploy": "echo '🚀 Начало процесса деплоя...' && docker login gitea.pena && if [ $? -eq 0 ]; then echo '✅ Успешный логин в Docker registry'; else echo '❌ Ошибка логина в Docker registry'; exit 1; fi && echo '🏗️ Сборка Docker образа...' && docker build -t gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest . && if [ $? -eq 0 ]; then echo '✅ Docker образ успешно собран'; else echo '❌ Ошибка сборки Docker образа'; exit 1; fi && echo '📤 Пуш образа в registry...' && docker push gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest && if [ $? -eq 0 ]; then echo '✅ Образ успешно загружен в registry'; echo '🎉 Деплой завершен успешно!'; else echo '❌ Ошибка загрузки образа в registry'; exit 1; fi", "deploy": "echo '🚀 Начало процесса деплоя...' && docker login gitea.pena && if [ $? -eq 0 ]; then echo '✅ Успешный логин в Docker registry'; else echo '❌ Ошибка логина в Docker registry'; exit 1; fi && echo '🏗️ Сборка Docker образа...' && docker build -t gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest . && if [ $? -eq 0 ]; then echo '✅ Docker образ успешно собран'; else echo '❌ Ошибка сборки Docker образа'; exit 1; fi && echo '📤 Пуш образа в registry...' && docker push gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest && if [ $? -eq 0 ]; then echo '✅ Образ успешно загружен в registry'; echo '🎉 Деплой завершен успешно!'; else echo '❌ Ошибка загрузки образа в registry'; exit 1; fi",
"prepare": "husky" "prepare": "husky",
"test": "cypress open"
}, },
"devDependencies": { "devDependencies": {
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
@ -37,7 +38,8 @@
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@mui/x-date-pickers": "^6.16.1", "@mui/x-date-pickers": "^6.16.1",
"@types/node": "^16.7.13", "@types/mocha": "^10.0.10",
"@types/node": "^20.19.19",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
@ -53,6 +55,7 @@
"husky": "^9.0.11", "husky": "^9.0.11",
"immer": "^10.0.3", "immer": "^10.0.3",
"lint-staged": "^15.2.5", "lint-staged": "^15.2.5",
"mocha": "^11.7.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"nanoid": "^5.0.3", "nanoid": "^5.0.3",
"notistack": "^3.0.1", "notistack": "^3.0.1",

760
yarn.lock

File diff suppressed because it is too large Load Diff