@crm-market/template-shared 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import { ref, computed } from 'vue';
2
+ import { useRuntimeConfig } from '#imports';
3
+ import { useSiteConfig } from './useSiteConfig';
2
4
 
3
5
  interface Category {
4
6
  id: string;
@@ -1,4 +1,7 @@
1
1
  import { ref } from 'vue';
2
+ import { useRuntimeConfig } from '#imports';
3
+ import { useCart } from './useCart';
4
+ import { useSiteConfig } from './useSiteConfig';
2
5
 
3
6
  interface ShippingAddress {
4
7
  firstName: string;
@@ -1,4 +1,6 @@
1
1
  import { ref, computed } from 'vue';
2
+ import { useRuntimeConfig } from '#imports';
3
+ import { useSiteConfig } from './useSiteConfig';
2
4
 
3
5
  interface Product {
4
6
  id: string;
@@ -1,4 +1,5 @@
1
1
  import { ref, computed } from 'vue';
2
+ import { useRuntimeConfig, useRoute } from '#imports';
2
3
 
3
4
  interface ColorScheme {
4
5
  primary?: string;
@@ -1,5 +1,6 @@
1
- import { computed } from 'vue';
1
+ import { ref, computed } from 'vue';
2
2
  import type { TemplateSection, ContentBlock } from '../types';
3
+ import { useSiteConfig } from './useSiteConfig';
3
4
 
4
5
  /**
5
6
  * Composable для управління секціями homepage.
@@ -0,0 +1,20 @@
1
+ import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir } from '@nuxt/kit';
2
+
3
+ export default defineNuxtModule({
4
+ meta: {
5
+ name: 'template-shared-auto-imports',
6
+ },
7
+ setup(_options, nuxt) {
8
+ const { resolve } = createResolver(import.meta.url);
9
+
10
+ // Реєстрація composables як auto-imports
11
+ addImportsDir(resolve('../composables'));
12
+ addImportsDir(resolve('../utils'));
13
+
14
+ // Реєстрація components
15
+ addComponentsDir({
16
+ path: resolve('../components'),
17
+ pathPrefix: false,
18
+ });
19
+ },
20
+ });
package/nuxt.config.ts CHANGED
@@ -42,6 +42,8 @@ export default defineNuxtConfig({
42
42
  },
43
43
 
44
44
  modules: [
45
+ // Auto-imports composables, utils, components для npm-пакету
46
+ resolve(__dirname, 'modules/auto-imports'),
45
47
  (
46
48
  _options: any,
47
49
  nuxt: {
@@ -81,16 +83,5 @@ export default defineNuxtConfig({
81
83
  },
82
84
  },
83
85
 
84
- components: [
85
- { path: resolve(__dirname, 'components'), pathPrefix: false },
86
- ],
87
-
88
- imports: {
89
- dirs: [
90
- resolve(__dirname, 'composables'),
91
- resolve(__dirname, 'utils'),
92
- ],
93
- },
94
-
95
86
  compatibilityDate: "2026-01-02",
96
87
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crm-market/template-shared",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "Shared Nuxt 3 layer for CRM Market store templates (layouts, components, composables, pages)",
6
6
  "publishConfig": {
@@ -21,18 +21,18 @@
21
21
  "plugins",
22
22
  "types",
23
23
  "utils",
24
- "e2e"
24
+ "modules"
25
25
  ],
26
26
  "dependencies": {
27
27
  "bootstrap": "^5.3.3",
28
28
  "nuxt": "^3.12.4",
29
29
  "nuxt-swiper": "^1.2.2",
30
30
  "vue": "^3.4.34",
31
- "vue-router": "^4.4.0"
31
+ "vue-router": "^4.4.0",
32
+ "vuetify": "^3.6.13",
33
+ "vite-plugin-vuetify": "^2.0.3"
32
34
  },
33
35
  "devDependencies": {
34
- "sass": "^1.77.8",
35
- "vite-plugin-vuetify": "^2.0.3",
36
- "vuetify": "^3.6.13"
36
+ "sass": "^1.77.8"
37
37
  }
38
38
  }
@@ -1,3 +1,8 @@
1
+ import { defineNuxtPlugin } from '#app';
2
+ import { useSiteConfig } from '../composables/useSiteConfig';
3
+ import { useCart } from '../composables/useCart';
4
+ import { useCategories } from '../composables/useCategories';
5
+
1
6
  export default defineNuxtPlugin(async () => {
2
7
  const { fetchConfig, error } = useSiteConfig();
3
8
  const { loadCart } = useCart();
package/utils/image.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { useRuntimeConfig } from '#imports';
2
+
1
3
  /**
2
4
  * Перетворює відносні шляхи до зображень (/media/...) у повні URL
3
5
  */
package/e2e/cart.spec.ts DELETED
@@ -1,71 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { setupMockApi } from './fixtures/mock-api';
3
- import { CartPage } from './pages/cart.page';
4
-
5
- test.describe('Кошик', () => {
6
- let cartPage: CartPage;
7
-
8
- test.beforeEach(async ({ page }) => {
9
- await setupMockApi(page);
10
- cartPage = new CartPage(page);
11
- });
12
-
13
- test('порожній кошик при першому відвідуванні', async () => {
14
- await cartPage.goto();
15
- // Кошик має бути порожнім або показувати повідомлення
16
- const isEmpty = await cartPage.isEmpty();
17
- const itemCount = await cartPage.getItemCount();
18
- expect(isEmpty || itemCount === 0).toBeTruthy();
19
- });
20
-
21
- test('відображає кошик з товарами після додавання', async ({ page }) => {
22
- // Додаємо товар у localStorage перед переходом
23
- await page.goto('/');
24
- await page.evaluate(() => {
25
- const cart = {
26
- items: [{
27
- productId: 'product-1',
28
- name: 'Тестовий товар 1',
29
- price: 1500,
30
- discountPrice: 1200,
31
- quantity: 2,
32
- image: '/media/test/product-1.png',
33
- }],
34
- updatedAt: Date.now(),
35
- };
36
- localStorage.setItem('shopping_cart', JSON.stringify(cart));
37
- });
38
-
39
- await cartPage.goto();
40
- await page.waitForTimeout(1000);
41
-
42
- // Має бути хоча б один товар
43
- const count = await cartPage.getItemCount();
44
- expect(count).toBeGreaterThanOrEqual(1);
45
- });
46
-
47
- test('має кнопку оформлення замовлення', async ({ page }) => {
48
- // Додаємо товар у localStorage
49
- await page.goto('/');
50
- await page.evaluate(() => {
51
- const cart = {
52
- items: [{
53
- productId: 'product-1',
54
- name: 'Тестовий товар',
55
- price: 1500,
56
- quantity: 1,
57
- }],
58
- updatedAt: Date.now(),
59
- };
60
- localStorage.setItem('shopping_cart', JSON.stringify(cart));
61
- });
62
-
63
- await cartPage.goto();
64
- await page.waitForTimeout(1000);
65
-
66
- const checkoutBtn = cartPage.checkoutButton;
67
- if (await checkoutBtn.isVisible()) {
68
- await expect(checkoutBtn).toBeEnabled();
69
- }
70
- });
71
- });
@@ -1,166 +0,0 @@
1
- import type { Page } from '@playwright/test';
2
-
3
- /**
4
- * Фікстура для моку API відповідей.
5
- * Використовується для тестування без реального бекенду.
6
- */
7
-
8
- export const MOCK_SITE_CONFIG = {
9
- id: 'test-config-id',
10
- organizationId: 'test-org-id',
11
- publicApiToken: 'pub_test_token_12345',
12
- siteName: 'Тестовий магазин',
13
- siteDescription: 'Інтернет-магазин для тестування',
14
- logoUrl: '/media/test/logo.png',
15
- faviconUrl: '/media/test/favicon.ico',
16
- heroImageUrl: '/media/test/hero.png',
17
- contactInfo: {
18
- phone: '+380501234567',
19
- email: 'test@example.com',
20
- address: 'м. Київ, вул. Тестова, 1',
21
- socialMedia: {
22
- facebook: 'https://facebook.com/test',
23
- instagram: 'https://instagram.com/test',
24
- },
25
- },
26
- subdomain: 'test-shop',
27
- isPublished: true,
28
- template: {
29
- code: 'electronics',
30
- name: 'Electronics',
31
- type: 'electronics',
32
- },
33
- customization: {
34
- colors: {
35
- primary: '#5093f7',
36
- secondary: '#22262a',
37
- accent: '#f7931e',
38
- },
39
- contentBlocks: [
40
- { key: 'todayBestDeals', order: 1, enabled: true },
41
- { key: 'thisMonthSales', order: 2, enabled: true },
42
- ],
43
- },
44
- };
45
-
46
- export const MOCK_PRODUCTS = {
47
- products: [
48
- {
49
- id: 'product-1',
50
- name: 'Тестовий товар 1',
51
- price: 1500,
52
- discountPrice: 1200,
53
- images: ['/media/test/product-1.png'],
54
- categoryId: 'cat-1',
55
- organizationId: 'test-org-id',
56
- isActive: true,
57
- stock: 10,
58
- rating: 4.5,
59
- reviews: 12,
60
- },
61
- {
62
- id: 'product-2',
63
- name: 'Тестовий товар 2',
64
- price: 2500,
65
- images: ['/media/test/product-2.png'],
66
- categoryId: 'cat-1',
67
- organizationId: 'test-org-id',
68
- isActive: true,
69
- stock: 5,
70
- rating: 4.0,
71
- reviews: 8,
72
- },
73
- {
74
- id: 'product-3',
75
- name: 'Тестовий товар 3',
76
- price: 800,
77
- discountPrice: 600,
78
- images: ['/media/test/product-3.png'],
79
- categoryId: 'cat-2',
80
- organizationId: 'test-org-id',
81
- isActive: true,
82
- stock: 20,
83
- rating: 3.8,
84
- reviews: 5,
85
- },
86
- ],
87
- total: 3,
88
- };
89
-
90
- export const MOCK_CATEGORIES = {
91
- categories: [
92
- {
93
- id: 'cat-1',
94
- name: 'Електроніка',
95
- slug: 'electronics',
96
- image: '/media/test/cat-1.png',
97
- organizationId: 'test-org-id',
98
- isActive: true,
99
- order: 1,
100
- },
101
- {
102
- id: 'cat-2',
103
- name: 'Аксесуари',
104
- slug: 'accessories',
105
- image: '/media/test/cat-2.png',
106
- organizationId: 'test-org-id',
107
- isActive: true,
108
- order: 2,
109
- },
110
- ],
111
- };
112
-
113
- /**
114
- * Налаштовує моки API для сторінки.
115
- */
116
- export async function setupMockApi(page: Page) {
117
- // Мок конфігурації сайту
118
- await page.route('**/site-template/public/config/subdomain/**', async (route) => {
119
- await route.fulfill({
120
- status: 200,
121
- contentType: 'application/json',
122
- body: JSON.stringify(MOCK_SITE_CONFIG),
123
- });
124
- });
125
-
126
- // Мок товарів
127
- await page.route('**/site-template/public/store/products*', async (route) => {
128
- const url = new URL(route.request().url());
129
- const productId = url.pathname.match(/products\/(.+)/)?.[1];
130
-
131
- if (productId) {
132
- const product = MOCK_PRODUCTS.products.find(p => p.id === productId);
133
- await route.fulfill({
134
- status: product ? 200 : 404,
135
- contentType: 'application/json',
136
- body: JSON.stringify(product ? { product } : { error: 'Not found' }),
137
- });
138
- } else {
139
- await route.fulfill({
140
- status: 200,
141
- contentType: 'application/json',
142
- body: JSON.stringify(MOCK_PRODUCTS),
143
- });
144
- }
145
- });
146
-
147
- // Мок категорій
148
- await page.route('**/site-template/public/store/categories*', async (route) => {
149
- await route.fulfill({
150
- status: 200,
151
- contentType: 'application/json',
152
- body: JSON.stringify(MOCK_CATEGORIES),
153
- });
154
- });
155
-
156
- // Мок замовлень
157
- await page.route('**/order', async (route) => {
158
- if (route.request().method() === 'POST') {
159
- await route.fulfill({
160
- status: 201,
161
- contentType: 'application/json',
162
- body: JSON.stringify({ id: 'order-test-123' }),
163
- });
164
- }
165
- });
166
- }
@@ -1,65 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { setupMockApi } from './fixtures/mock-api';
3
- import { HomepagePage } from './pages/homepage.page';
4
-
5
- test.describe('Головна сторінка', () => {
6
- let homepage: HomepagePage;
7
-
8
- test.beforeEach(async ({ page }) => {
9
- await setupMockApi(page);
10
- homepage = new HomepagePage(page);
11
- await homepage.goto();
12
- });
13
-
14
- test('відображає header з логотипом', async ({ page }) => {
15
- // Header має бути видимим
16
- await expect(page.locator('.middle-header-area, .header-area').first()).toBeVisible();
17
- });
18
-
19
- test('відображає банер', async ({ page }) => {
20
- // Банер або swiper має бути видимим
21
- await expect(page.locator('.banner-area, .swiper, .banner-wrapper').first()).toBeVisible();
22
- });
23
-
24
- test('відображає footer', async ({ page }) => {
25
- // Скролимо вниз і перевіряємо footer
26
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
27
- await expect(page.locator('.footer-area, footer').first()).toBeVisible();
28
- });
29
-
30
- test('має коректний title з конфігурації', async ({ page }) => {
31
- // Чекаємо поки title оновиться з API конфігу
32
- await page.waitForTimeout(2000);
33
- const title = await page.title();
34
- expect(title).toBeTruthy();
35
- });
36
-
37
- test('навігація до сторінки товарів', async ({ page }) => {
38
- // Шукаємо посилання на товари
39
- const productsLink = page.locator('a[href*="products"], a:has-text("Products"), a:has-text("Товари")').first();
40
- if (await productsLink.isVisible()) {
41
- await productsLink.click();
42
- await expect(page).toHaveURL(/products/);
43
- }
44
- });
45
-
46
- test('навігація до кошика', async ({ page }) => {
47
- const cartLink = page.locator('a[href*="cart"], .cart-btn').first();
48
- if (await cartLink.isVisible()) {
49
- await cartLink.click();
50
- await expect(page).toHaveURL(/cart/);
51
- }
52
- });
53
- });
54
-
55
- test.describe('SSR', () => {
56
- test('сторінка має серверний HTML контент', async ({ page }) => {
57
- await setupMockApi(page);
58
- // Перевіряємо що HTML рендериться server-side (не порожній body)
59
- const response = await page.goto('/');
60
- const html = await response?.text();
61
- expect(html).toContain('<!DOCTYPE html>');
62
- // Має містити хоча б базову розмітку
63
- expect(html).toContain('<div');
64
- });
65
- });
@@ -1,73 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { setupMockApi } from './fixtures/mock-api';
3
-
4
- test.describe('Layout — шари візуалізації', () => {
5
- test.beforeEach(async ({ page }) => {
6
- await setupMockApi(page);
7
- });
8
-
9
- test('головна сторінка: правильний порядок шарів', async ({ page }) => {
10
- await page.goto('/');
11
-
12
- // Перевіряємо порядок: TopHeader -> MiddleHeader -> Navbar -> Content -> Footer
13
- const body = page.locator('body');
14
- const html = await body.innerHTML();
15
-
16
- // TopHeader має бути перед MiddleHeader
17
- const topHeaderPos = html.indexOf('top-header');
18
- const middleHeaderPos = html.indexOf('middle-header');
19
- const navbarPos = html.indexOf('navbar-area');
20
-
21
- if (topHeaderPos > -1 && middleHeaderPos > -1) {
22
- expect(topHeaderPos).toBeLessThan(middleHeaderPos);
23
- }
24
- if (middleHeaderPos > -1 && navbarPos > -1) {
25
- expect(middleHeaderPos).toBeLessThan(navbarPos);
26
- }
27
- });
28
-
29
- test('внутрішня сторінка: має page banner', async ({ page }) => {
30
- await page.goto('/about-us');
31
-
32
- // Inner layout має мати page banner замість основного banner
33
- const pageBanner = page.locator('.page-banner-area, .breadcrumb-area').first();
34
- await expect(pageBanner).toBeVisible({ timeout: 5000 });
35
- });
36
-
37
- test('footer містить контактну інформацію', async ({ page }) => {
38
- await page.goto('/');
39
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
40
-
41
- const footer = page.locator('.footer-area, footer').first();
42
- await expect(footer).toBeVisible();
43
- });
44
-
45
- test('back-to-top кнопка з\'являється при скролі', async ({ page }) => {
46
- await page.goto('/');
47
-
48
- // Скролимо вниз
49
- await page.evaluate(() => window.scrollTo(0, 1000));
50
- await page.waitForTimeout(1000);
51
-
52
- const backToTop = page.locator('.back-to-top-btn, .scroll-top, #scrollTop').first();
53
- if (await backToTop.count() > 0) {
54
- await expect(backToTop).toBeVisible({ timeout: 3000 });
55
- }
56
- });
57
- });
58
-
59
- test.describe('Layout — динамічний вибір компонентів', () => {
60
- test('CSS змінні застосовуються з конфігурації', async ({ page }) => {
61
- await setupMockApi(page);
62
- await page.goto('/');
63
- await page.waitForTimeout(2000);
64
-
65
- // Перевіряємо що CSS змінні встановлені
66
- const primaryColor = await page.evaluate(() => {
67
- return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim();
68
- });
69
-
70
- // Має бути або з API (#5093f7) або дефолтне значення
71
- expect(primaryColor).toBeTruthy();
72
- });
73
- });
@@ -1,61 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { setupMockApi } from './fixtures/mock-api';
3
-
4
- test.describe('Навігація між сторінками', () => {
5
- test.beforeEach(async ({ page }) => {
6
- await setupMockApi(page);
7
- });
8
-
9
- const pages = [
10
- { path: '/about-us', title: 'Про нас' },
11
- { path: '/contact', title: 'Контакти' },
12
- { path: '/products', title: 'Товари' },
13
- { path: '/categories', title: 'Категорії' },
14
- { path: '/cart', title: 'Кошик' },
15
- { path: '/login', title: 'Увійти' },
16
- { path: '/register', title: 'Реєстрація' },
17
- { path: '/faq', title: 'Часті запитання' },
18
- { path: '/terms-conditions', title: 'Умови' },
19
- { path: '/privacy-policy', title: 'Політика' },
20
- ];
21
-
22
- for (const p of pages) {
23
- test(`відкривається сторінка ${p.path}`, async ({ page }) => {
24
- const response = await page.goto(p.path);
25
- expect(response?.status()).toBeLessThan(500);
26
-
27
- // Сторінка має рендеритись без помилок
28
- const errorOverlay = page.locator('#nuxt-error, .nuxt-error-page');
29
- const hasError = await errorOverlay.isVisible().catch(() => false);
30
-
31
- if (!hasError) {
32
- // Перевіряємо наявність page banner
33
- const banner = page.locator('.page-banner-area, .breadcrumb').first();
34
- await expect(banner).toBeVisible({ timeout: 5000 }).catch(() => {
35
- // Деякі сторінки можуть не мати banner — це ок
36
- });
37
- }
38
- });
39
- }
40
- });
41
-
42
- test.describe('Responsive навігація', () => {
43
- test.beforeEach(async ({ page }) => {
44
- await setupMockApi(page);
45
- });
46
-
47
- test('мобільне меню відкривається', async ({ page }) => {
48
- // Встановлюємо мобільний viewport
49
- await page.setViewportSize({ width: 375, height: 812 });
50
- await page.goto('/');
51
-
52
- // Шукаємо кнопку мобільного меню
53
- const menuButton = page.locator('.navbar-toggler, .hamburger-menu, .ri-menu-line, button[aria-label*="menu"]').first();
54
- if (await menuButton.isVisible()) {
55
- await menuButton.click();
56
- // Перевіряємо що мобільне меню з'явилось
57
- const mobileMenu = page.locator('.responsive-navbar, .mobile-menu, .offcanvas, .sidebar-menu').first();
58
- await expect(mobileMenu).toBeVisible({ timeout: 3000 });
59
- }
60
- });
61
- });
@@ -1,44 +0,0 @@
1
- import type { Page, Locator } from '@playwright/test';
2
-
3
- /**
4
- * Page Object для сторінки кошика.
5
- */
6
- export class CartPage {
7
- readonly page: Page;
8
- readonly cartItems: Locator;
9
- readonly emptyCartMessage: Locator;
10
- readonly subtotal: Locator;
11
- readonly checkoutButton: Locator;
12
- readonly removeButtons: Locator;
13
- readonly quantityInputs: Locator;
14
-
15
- constructor(page: Page) {
16
- this.page = page;
17
- this.cartItems = page.locator('.cart-item, .cart-table tbody tr');
18
- this.emptyCartMessage = page.locator('.empty-cart, :text("порожній"), :text("empty")');
19
- this.subtotal = page.locator('.subtotal, .cart-total');
20
- this.checkoutButton = page.locator('a[href*="checkout"], button:has-text("Checkout"), button:has-text("Оформити")');
21
- this.removeButtons = page.locator('.remove-btn, .ri-delete-bin-line, .ri-close-line');
22
- this.quantityInputs = page.locator('.quantity-input, input[type="number"]');
23
- }
24
-
25
- async goto() {
26
- await this.page.goto('/cart');
27
- }
28
-
29
- async getItemCount() {
30
- return await this.cartItems.count();
31
- }
32
-
33
- async removeItem(index: number) {
34
- await this.removeButtons.nth(index).click();
35
- }
36
-
37
- async proceedToCheckout() {
38
- await this.checkoutButton.click();
39
- }
40
-
41
- async isEmpty() {
42
- return await this.emptyCartMessage.isVisible();
43
- }
44
- }
@@ -1,46 +0,0 @@
1
- import type { Page, Locator } from '@playwright/test';
2
-
3
- /**
4
- * Page Object для головної сторінки шаблону.
5
- */
6
- export class HomepagePage {
7
- readonly page: Page;
8
- readonly topHeader: Locator;
9
- readonly middleHeader: Locator;
10
- readonly navbar: Locator;
11
- readonly banner: Locator;
12
- readonly footer: Locator;
13
- readonly searchInput: Locator;
14
- readonly cartIcon: Locator;
15
- readonly logo: Locator;
16
-
17
- constructor(page: Page) {
18
- this.page = page;
19
- this.topHeader = page.locator('.top-header-area');
20
- this.middleHeader = page.locator('.middle-header-area');
21
- this.navbar = page.locator('.navbar-area');
22
- this.banner = page.locator('.banner-area, .swiper');
23
- this.footer = page.locator('.footer-area');
24
- this.searchInput = page.locator('.search-input, input[type="search"], .search-box input');
25
- this.cartIcon = page.locator('.cart-btn, .ri-shopping-cart-line').first();
26
- this.logo = page.locator('.logo img, .navbar-brand img').first();
27
- }
28
-
29
- async goto() {
30
- await this.page.goto('/');
31
- }
32
-
33
- async searchProduct(query: string) {
34
- await this.searchInput.fill(query);
35
- await this.searchInput.press('Enter');
36
- }
37
-
38
- async openCart() {
39
- await this.cartIcon.click();
40
- }
41
-
42
- async getSectionCount() {
43
- // Підраховує кількість видимих секцій на головній сторінці
44
- return await this.page.locator('section, .section-area, [class*="area"]').count();
45
- }
46
- }
@@ -1,32 +0,0 @@
1
- import { defineConfig, devices } from '@playwright/test';
2
-
3
- /**
4
- * Базова конфігурація Playwright для шаблонів магазинів.
5
- * Кожен шаблон може розширити цю конфігурацію, змінивши baseURL та порт.
6
- */
7
- export default defineConfig({
8
- testDir: '.',
9
- fullyParallel: true,
10
- forbidOnly: !!process.env.CI,
11
- retries: process.env.CI ? 2 : 0,
12
- workers: process.env.CI ? 1 : undefined,
13
- reporter: 'html',
14
- timeout: 30_000,
15
-
16
- use: {
17
- baseURL: process.env.BASE_URL || 'http://localhost:3000',
18
- trace: 'on-first-retry',
19
- screenshot: 'only-on-failure',
20
- },
21
-
22
- projects: [
23
- {
24
- name: 'chromium',
25
- use: { ...devices['Desktop Chrome'] },
26
- },
27
- {
28
- name: 'mobile',
29
- use: { ...devices['iPhone 14'] },
30
- },
31
- ],
32
- });
@@ -1,33 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { setupMockApi } from './fixtures/mock-api';
3
- import { ProductsPage } from './pages/products.page';
4
-
5
- test.describe('Сторінка товарів', () => {
6
- let productsPage: ProductsPage;
7
-
8
- test.beforeEach(async ({ page }) => {
9
- await setupMockApi(page);
10
- productsPage = new ProductsPage(page);
11
- await productsPage.goto();
12
- });
13
-
14
- test('відображає банер сторінки', async ({ page }) => {
15
- await expect(page.locator('.page-banner-area, .breadcrumb').first()).toBeVisible();
16
- });
17
-
18
- test('відображає товари з API', async () => {
19
- // Чекаємо завантаження товарів
20
- await productsPage.page.waitForTimeout(2000);
21
- const count = await productsPage.getProductCount();
22
- expect(count).toBeGreaterThan(0);
23
- });
24
-
25
- test('товар має назву та ціну', async ({ page }) => {
26
- await page.waitForTimeout(2000);
27
- // Перевіряємо що є текст з цінами (грн або числа)
28
- const priceElements = page.locator('.product-card .price, .product-item .price, .product-price');
29
- if (await priceElements.count() > 0) {
30
- await expect(priceElements.first()).toBeVisible();
31
- }
32
- });
33
- });