| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- import { LanguageCode } from '@vendure/common/lib/generated-types';
- import { createTestEnvironment } from '@vendure/testing';
- import path from 'path';
- import { afterAll, beforeAll, describe, expect, it } from 'vitest';
- import { initialData } from '../../../e2e-common/e2e-initial-data';
- import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
- import { graphql } from './graphql/graphql-admin';
- import { createCollectionDocument, createProductDocument } from './graphql/shared-definitions';
- describe('Slug generation', () => {
- const { server, adminClient } = createTestEnvironment(testConfig());
- beforeAll(async () => {
- await server.init({
- initialData,
- productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
- customerCount: 1,
- });
- await adminClient.asSuperAdmin();
- }, TEST_SETUP_TIMEOUT_MS);
- afterAll(async () => {
- await server.destroy();
- });
- describe('slugForEntity query', () => {
- describe('basic slug generation', () => {
- it('generates a simple slug', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Test Product',
- },
- });
- expect(result.slugForEntity).toBe('test-product');
- });
- it('handles multiple spaces', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Test Product Name',
- },
- });
- expect(result.slugForEntity).toBe('test-product-name');
- });
- it('converts uppercase to lowercase', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'TEST PRODUCT NAME',
- },
- });
- expect(result.slugForEntity).toBe('test-product-name');
- });
- it('preserves numbers', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Product 123 Version 2',
- },
- });
- expect(result.slugForEntity).toBe('product-123-version-2');
- });
- });
- describe('special characters and unicode', () => {
- it('removes special characters', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Product!@#$%^&*()_+Name',
- },
- });
- expect(result.slugForEntity).toBe('productname');
- });
- it('handles special characters with spaces', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Product !@#$ Name',
- },
- });
- expect(result.slugForEntity).toBe('product-name');
- });
- it('handles diacritical marks (accents)', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Café Français naïve résumé',
- },
- });
- expect(result.slugForEntity).toBe('cafe-francais-naive-resume');
- });
- it('handles German umlauts', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Über größer schön',
- },
- });
- expect(result.slugForEntity).toBe('uber-groer-schon');
- });
- it('handles Spanish characters', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Niño Español Añejo',
- },
- });
- expect(result.slugForEntity).toBe('nino-espanol-anejo');
- });
- it('handles non-Latin scripts (removes them)', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Product 商品 المنتج उत्पाद',
- },
- });
- expect(result.slugForEntity).toBe('product');
- });
- it('handles emoji (removes them)', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Cool Product 😎 🚀 Amazing',
- },
- });
- expect(result.slugForEntity).toBe('cool-product-amazing');
- });
- it('handles punctuation and symbols', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Product: The Best! (Version 2.0) - New & Improved',
- },
- });
- expect(result.slugForEntity).toBe('product-the-best-version-20-new-improved');
- });
- });
- describe('edge cases', () => {
- it('handles leading and trailing spaces', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: ' Test Product ',
- },
- });
- expect(result.slugForEntity).toBe('test-product');
- });
- it('handles hyphens correctly', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Test--Product---Name',
- },
- });
- expect(result.slugForEntity).toBe('test-product-name');
- });
- it('handles leading and trailing hyphens', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: '-Test Product-',
- },
- });
- expect(result.slugForEntity).toBe('test-product');
- });
- it('handles empty string', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: '',
- },
- });
- expect(result.slugForEntity).toBe('');
- });
- it('handles only special characters', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: '!@#$%^&*()',
- },
- });
- expect(result.slugForEntity).toBe('');
- });
- it('handles mixed case with numbers and special chars', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: '100% Natural & Organic Product #1',
- },
- });
- expect(result.slugForEntity).toBe('100-natural-organic-product-1');
- });
- });
- describe('uniqueness handling', () => {
- it('appends number for duplicate slugs', async () => {
- // First, create a product with slug 'laptop'
- await adminClient.query(createProductDocument, {
- input: {
- translations: [
- {
- languageCode: 'en',
- name: 'Laptop',
- slug: 'laptop',
- description: 'A laptop computer',
- },
- ],
- },
- });
- // Now try to generate slug for another product with the same base slug
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Laptop',
- },
- });
- expect(result.slugForEntity).toBe('laptop-1');
- });
- it('increments counter for multiple duplicates', async () => {
- // Create products with slugs 'phone' and 'phone-1'
- await adminClient.query(createProductDocument, {
- input: {
- translations: [
- {
- languageCode: 'en',
- name: 'Phone',
- slug: 'phone',
- description: 'A smartphone',
- },
- ],
- },
- });
- await adminClient.query(createProductDocument, {
- input: {
- translations: [
- {
- languageCode: 'en',
- name: 'Phone 2',
- slug: 'phone-1',
- description: 'Another smartphone',
- },
- ],
- },
- });
- // Now generate slug for another phone
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Phone',
- },
- });
- expect(result.slugForEntity).toBe('phone-2');
- });
- it('excludes own ID when checking uniqueness', async () => {
- // Create a product
- const createResult = await adminClient.query(createProductDocument, {
- input: {
- translations: [
- {
- languageCode: 'en',
- name: 'Tablet',
- slug: 'tablet',
- description: 'A tablet device',
- },
- ],
- },
- });
- const productId = createResult.createProduct.id;
- // Generate slug for the same product (updating scenario)
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Tablet',
- entityId: productId,
- },
- });
- // Should return the same slug without appending number
- expect(result.slugForEntity).toBe('tablet');
- });
- it('works with different entity types', async () => {
- // Test with Collection entity (slug field is in CollectionTranslation)
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Collection',
- fieldName: 'slug',
- inputValue: 'Summer Collection 2024',
- },
- });
- expect(result.slugForEntity).toBe('summer-collection-2024');
- });
- it('handles multi-language slug generation', async () => {
- // Create a product with English translation first
- const createProduct = await adminClient.query(createProductDocument, {
- input: {
- translations: [
- {
- languageCode: LanguageCode.en,
- name: 'English Product',
- slug: 'english-product',
- description: 'Product in English',
- },
- ],
- },
- });
- const productId = createProduct.createProduct.id;
- // Test generating slug for German translation of the same product
- const germanResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Deutsches Produkt',
- entityId: productId,
- },
- });
- expect(germanResult.slugForEntity).toBe('deutsches-produkt');
- // Test generating slug for French translation
- const frenchResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Produit Français',
- entityId: productId,
- },
- });
- expect(frenchResult.slugForEntity).toBe('produit-francais');
- });
- it('handles uniqueness across different language translations', async () => {
- // Create first product with multiple language translations
- await adminClient.query(createProductDocument, {
- input: {
- translations: [
- {
- languageCode: LanguageCode.en,
- name: 'Computer',
- slug: 'computer',
- description: 'A computer',
- },
- {
- languageCode: LanguageCode.de,
- name: 'Computer',
- slug: 'computer-de',
- description: 'Ein Computer',
- },
- ],
- },
- });
- // Generate slug for a new product with same English name
- const englishSlugResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Computer',
- },
- });
- expect(englishSlugResult.slugForEntity).toBe('computer-1');
- // Generate slug with German input that also conflicts
- const germanSlugResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Computer DE',
- },
- });
- expect(germanSlugResult.slugForEntity).toBe('computer-de-1');
- // Generate slug with French input that doesn't conflict
- const frenchSlugResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Ordinateur',
- },
- });
- expect(frenchSlugResult.slugForEntity).toBe('ordinateur');
- });
- it('handles translation entity exclusion correctly with multiple languages', async () => {
- // Create a product with multiple language translations
- const createProduct = await adminClient.query(createProductDocument, {
- input: {
- translations: [
- {
- languageCode: LanguageCode.en,
- name: 'Multilingual Product',
- slug: 'multilingual-product',
- description: 'Product in English',
- },
- {
- languageCode: LanguageCode.de,
- name: 'Mehrsprachiges Produkt',
- slug: 'mehrsprachiges-produkt',
- description: 'Produkt auf Deutsch',
- },
- ],
- },
- });
- const productId = createProduct.createProduct.id;
- // Update English translation - should not conflict with itself
- const englishUpdateResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Multilingual Product Updated',
- entityId: productId,
- },
- });
- expect(englishUpdateResult.slugForEntity).toBe('multilingual-product-updated');
- // Update German translation - should not conflict with itself
- const germanUpdateResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Mehrsprachiges Produkt Aktualisiert',
- entityId: productId,
- },
- });
- expect(germanUpdateResult.slugForEntity).toBe('mehrsprachiges-produkt-aktualisiert');
- });
- });
- describe('multi-language collections', () => {
- it('generates unique slugs for collection translations', async () => {
- // Create a collection with multiple language translations
- const createCollection = await adminClient.query(createCollectionDocument, {
- input: {
- translations: [
- {
- languageCode: LanguageCode.en,
- name: 'Tech Collection',
- slug: 'tech-collection',
- description: 'Technology products',
- },
- {
- languageCode: LanguageCode.fr,
- name: 'Collection Tech',
- slug: 'collection-tech',
- description: 'Produits technologiques',
- },
- ],
- filters: [],
- },
- });
- const collectionId = createCollection.createCollection.id;
- // Test generating new slug for Spanish translation
- const spanishResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Collection',
- fieldName: 'slug',
- inputValue: 'Colección Tecnológica',
- entityId: collectionId,
- },
- });
- expect(spanishResult.slugForEntity).toBe('coleccion-tecnologica');
- });
- it('handles collection slug conflicts across languages', async () => {
- // Create collection with English name
- await adminClient.query(createCollectionDocument, {
- input: {
- translations: [
- {
- languageCode: LanguageCode.en,
- name: 'Fashion Collection',
- slug: 'fashion-collection',
- description: 'Fashion items',
- },
- ],
- filters: [],
- },
- });
- // Generate slug for another collection with similar name
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Collection',
- fieldName: 'slug',
- inputValue: 'Fashion Collection',
- },
- });
- expect(result.slugForEntity).toBe('fashion-collection-1');
- // Test with international name that transliterates to similar slug
- const internationalResult = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Collection',
- fieldName: 'slug',
- inputValue: 'Façhion Collêction',
- },
- });
- expect(internationalResult.slugForEntity).toBe('fachion-collection');
- });
- });
- describe('international character handling', () => {
- it('handles various language scripts in slug generation', async () => {
- // Test different language inputs
- const testCases = [
- { input: 'Café Français', expected: 'cafe-francais' },
- { input: 'Niño Español', expected: 'nino-espanol' },
- { input: 'Größer Schön', expected: 'groer-schon' },
- { input: 'Naïve Résumé', expected: 'naive-resume' },
- { input: 'Crème Brûlée', expected: 'creme-brulee' },
- { input: 'Piñata Jalapeño', expected: 'pinata-jalapeno' },
- ];
- for (const testCase of testCases) {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: testCase.input,
- },
- });
- expect(result.slugForEntity).toBe(testCase.expected);
- }
- });
- it('handles mixed language input', async () => {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'English Français Español Deutsch Mix',
- },
- });
- expect(result.slugForEntity).toBe('english-francais-espanol-deutsch-mix');
- });
- });
- describe('auto-detection functionality', () => {
- it('auto-detects translation entity for slug field', async () => {
- // Using base entity name, should automatically detect ProductTranslation
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: 'Auto Detection Test',
- },
- });
- expect(result.slugForEntity).toBe('auto-detection-test');
- });
- it('works with explicit translation entity names', async () => {
- // Still works when explicitly using translation entity name
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'ProductTranslation',
- fieldName: 'slug',
- inputValue: 'Explicit Translation Test',
- },
- });
- expect(result.slugForEntity).toBe('explicit-translation-test');
- });
- it('works with Collection entity auto-detection', async () => {
- // Using base entity name, should automatically detect CollectionTranslation
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Collection',
- fieldName: 'slug',
- inputValue: 'Collection Auto Detection',
- },
- });
- expect(result.slugForEntity).toBe('collection-auto-detection');
- });
- it('auto-detects translation entities for different languages', async () => {
- // Test that auto-detection works regardless of the intended language
- const testCases = [
- { input: 'Auto Detection English', expected: 'auto-detection-english' },
- { input: 'Détection Automatique', expected: 'detection-automatique' },
- { input: 'Detección Automática', expected: 'deteccion-automatica' },
- { input: 'Automatische Erkennung', expected: 'automatische-erkennung' },
- ];
- for (const testCase of testCases) {
- const result = await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'slug',
- inputValue: testCase.input,
- },
- });
- expect(result.slugForEntity).toBe(testCase.expected);
- }
- });
- });
- describe('error handling', () => {
- it('throws error for non-existent entity', async () => {
- try {
- await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'NonExistentEntity',
- fieldName: 'slug',
- inputValue: 'Test',
- },
- });
- expect.fail('Should have thrown an error');
- } catch (error: any) {
- expect(error.message).toContain('error.entity-not-found');
- }
- });
- it('throws error for non-existent field', async () => {
- try {
- await adminClient.query(slugForEntityDocument, {
- input: {
- entityName: 'Product',
- fieldName: 'nonExistentField',
- inputValue: 'Test',
- },
- });
- expect.fail('Should have thrown an error');
- } catch (error: any) {
- expect(error.message).toContain('error.entity-has-no-field');
- }
- });
- });
- });
- });
- const slugForEntityDocument = graphql(`
- query SlugForEntity($input: SlugForEntityInput!) {
- slugForEntity(input: $input)
- }
- `);
|