diff --git a/cypress/e2e/index.cy.ts b/cypress/e2e/index.cy.ts index a4eff723..acc76029 100644 --- a/cypress/e2e/index.cy.ts +++ b/cypress/e2e/index.cy.ts @@ -1,14 +1,18 @@ globalThis.fetch.bind(globalThis); +const TEST_URL = 'http://localhost:5173/'; + describe('@storyblok/js', () => { + beforeEach(() => { + cy.visit(TEST_URL, { + onBeforeLoad(win) { + cy.spy(win.console, 'error').as('consoleError'); + }, + }); + }); + describe('RichText', () => { it('should print a console error if the SDK is not initialized', () => { - cy.visit('http://localhost:5173/', { - onBeforeLoad(win) { - cy.spy(win.console, 'error').as('consoleError'); - }, - }); - cy.get('.render-rich-text').click(); cy.get('@consoleError').should( 'be.calledWith', @@ -18,12 +22,6 @@ describe('@storyblok/js', () => { }); it('should render the HTML using the default schema and resolver', () => { - cy.visit('http://localhost:5173/', { - onBeforeLoad(win) { - cy.spy(win.console, 'error').as('consoleError'); - }, - }); - cy.get('.without-bridge').click(); cy.get('.render-rich-text').click(); cy.get('@consoleError').should('not.be.called'); @@ -34,8 +32,6 @@ describe('@storyblok/js', () => { }); it('should render the HTML using a custom global schema and resolver', () => { - cy.visit('http://localhost:5173/'); - cy.get('.init-custom-rich-text').click(); cy.get('.render-rich-text').click(); cy.get('#rich-text-container').should( @@ -45,8 +41,6 @@ describe('@storyblok/js', () => { }); it('should render the HTML using a one-time schema and resolver', () => { - cy.visit('http://localhost:5173/'); - cy.get('.without-bridge').click(); cy.get('.render-rich-text-options').click(); cy.get('#rich-text-container').should( @@ -58,17 +52,15 @@ describe('@storyblok/js', () => { describe('Bridge', () => { it('Is loaded by default', () => { - cy.visit('http://localhost:5173/?_storyblok_tk[timestamp]=1677494658'); + cy.visit(`${TEST_URL}?_storyblok_tk[timestamp]=1677494658`); cy.get('.with-bridge').click(); - cy.get('#storyblok-javascript-bridge').should('exist'); + cy.get('#storyblok-javascript-bridge') + .should('exist') + .and('have.attr', 'src') + .and('include', 'storyblok-v2-latest.js'); }); it('Is not loaded if options.bridge: false and no errors are printed', () => { - cy.visit('http://localhost:5173/', { - onBeforeLoad(win) { - cy.spy(win.console, 'error').as('consoleError'); - }, - }); cy.get('.without-bridge').click(); cy.get('#storyblok-javascript-bridge').should('not.exist'); cy.get('@consoleError').should('not.be.called'); @@ -77,18 +69,23 @@ describe('@storyblok/js', () => { describe('Bridge (added independently)', () => { it('Can be loaded', () => { - cy.visit('http://localhost:5173/'); - cy.get('.load-bridge').click(); - cy.get('#storyblok-javascript-bridge').should('exist'); - }); - it('Can be loaded just once', () => { - cy.visit('http://localhost:5173/'); - cy.get('.load-bridge').click(); - cy.wait(1000); - cy.get('.load-bridge').click(); + cy.visit(TEST_URL, { + // Handle failed loads gracefully + failOnStatusCode: false, + onBeforeLoad(win) { + cy.spy(win.console, 'error').as('consoleError'); + }, + }); + + cy.get('.load-bridge') + .should('be.visible') + .click(); + cy.get('#storyblok-javascript-bridge') - .should('exist') - .and('have.length', 1); + .should('exist'); + + cy.get('@consoleError') + .should('not.be.called'); }); }); }); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 00000000..cddc6144 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["cypress"], + "isolatedModules": false + }, + "include": ["**/*.ts"], + "exclude": [] +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 57e94adc..68c43083 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,5 +3,5 @@ import { storyblokLintConfig } from '@storyblok/eslint-config'; export default storyblokLintConfig({ vue: true, }, { - ignores: ['tests/unit/coverage/', 'dist/', 'cypress/', '**/*.d.ts', '**/node_modules/**', 'README.md'], + ignores: ['tests/unit/coverage/', 'dist/', '**/*.d.ts', '**/node_modules/**', 'README.md'], }); diff --git a/package.json b/package.json index 2cea45ff..7992597b 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,11 @@ "@storyblok/eslint-config": "^0.3.0", "@tsconfig/recommended": "^1.0.8", "@types/node": "^22.10.2", + "@vitest/ui": "2.1.8", "cypress": "^13.17.0", "eslint": "^9.17.0", "eslint-plugin-cypress": "^4.1.0", + "jsdom": "^25.0.1", "kolorist": "^1.8.0", "pathe": "^1.1.2", "simple-git-hooks": "^2.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0baa37c9..06bce14c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,13 +23,16 @@ importers: version: 19.6.0 '@storyblok/eslint-config': specifier: ^0.3.0 - version: 0.3.0(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)) + version: 0.3.0(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8) '@tsconfig/recommended': specifier: ^1.0.8 version: 1.0.8 '@types/node': specifier: ^22.10.2 version: 22.10.2 + '@vitest/ui': + specifier: 2.1.8 + version: 2.1.8(vitest@2.1.8) cypress: specifier: ^13.17.0 version: 13.17.0 @@ -39,6 +42,9 @@ importers: eslint-plugin-cypress: specifier: ^4.1.0 version: 4.1.0(eslint@9.17.0(jiti@2.4.2)) + jsdom: + specifier: ^25.0.1 + version: 25.0.1 kolorist: specifier: ^1.8.0 version: 1.8.0 @@ -68,7 +74,7 @@ importers: version: 0.2.3(vite@6.0.6(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) vitest: specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.2) + version: 2.1.8(@types/node@22.10.2)(@vitest/ui@2.1.8)(jsdom@25.0.1) playground/vanilla: devDependencies: @@ -832,6 +838,9 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -1132,6 +1141,11 @@ packages: '@vitest/spy@2.1.8': resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} + '@vitest/ui@2.1.8': + resolution: {integrity: sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==} + peerDependencies: + vitest: 2.1.8 + '@vitest/utils@2.1.8': resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} @@ -1209,6 +1223,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -1515,6 +1533,10 @@ packages: engines: {node: '>=4'} hasBin: true + cssstyle@4.1.0: + resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1531,6 +1553,10 @@ packages: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -1563,6 +1589,9 @@ packages: supports-color: optional: true + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -1952,6 +1981,17 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -2116,10 +2156,22 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-signature@1.4.0: resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -2128,6 +2180,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2201,6 +2257,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2246,6 +2305,15 @@ packages: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -2563,6 +2631,10 @@ packages: mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2593,6 +2665,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + object-inspect@1.13.3: resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} engines: {node: '>= 0.4'} @@ -2662,6 +2737,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2842,6 +2920,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2854,6 +2935,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -2898,6 +2983,10 @@ packages: resolution: {integrity: sha512-tgqwPUMDcNDhuf1Xf6KTUsyeqGdgKMhzaH4PAZZuzguOgTl5uuyeYe/8mWgAr6IBxB5V06uqEf6Dy37gIWDtDg==} hasBin: true + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3006,6 +3095,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.6.2: resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==} engines: {node: '>=12.20'} @@ -3034,6 +3126,10 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyglobby@0.2.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3065,10 +3161,18 @@ packages: resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.0.0: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3346,11 +3450,31 @@ packages: typescript: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + wait-on@8.0.1: resolution: {integrity: sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==} engines: {node: '>=12.0.0'} hasBin: true + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.1.0: + resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3376,10 +3500,29 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3420,7 +3563,7 @@ packages: snapshots: - '@antfu/eslint-config@3.6.2(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2))': + '@antfu/eslint-config@3.6.2(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8)': dependencies: '@antfu/install-pkg': 0.4.1 '@clack/prompts': 0.7.0 @@ -3429,7 +3572,7 @@ snapshots: '@stylistic/eslint-plugin': 2.11.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) '@typescript-eslint/eslint-plugin': 8.17.0(@typescript-eslint/parser@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) '@typescript-eslint/parser': 8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) - '@vitest/eslint-plugin': 1.1.14(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)) + '@vitest/eslint-plugin': 1.1.14(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8) eslint: 9.17.0(jiti@2.4.2) eslint-config-flat-gitignore: 0.3.0(eslint@9.17.0(jiti@2.4.2)) eslint-flat-config-utils: 0.4.0 @@ -4011,6 +4154,8 @@ snapshots: '@pkgr/core@0.1.1': {} + '@polka/url@1.0.0-next.28': {} + '@rollup/pluginutils@5.1.4(rollup@4.28.0)': dependencies: '@types/estree': 1.0.6 @@ -4115,9 +4260,9 @@ snapshots: '@sideway/pinpoint@2.0.0': {} - '@storyblok/eslint-config@0.3.0(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2))': + '@storyblok/eslint-config@0.3.0(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8)': dependencies: - '@antfu/eslint-config': 3.6.2(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2)) + '@antfu/eslint-config': 3.6.2(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8) eslint: 9.17.0(jiti@2.4.2) eslint-plugin-format: 0.1.3(eslint@9.17.0(jiti@2.4.2)) transitivePeerDependencies: @@ -4285,13 +4430,13 @@ snapshots: vite: 6.0.2(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) vue: 3.5.13(typescript@5.7.2) - '@vitest/eslint-plugin@1.1.14(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8(@types/node@22.10.2))': + '@vitest/eslint-plugin@1.1.14(@typescript-eslint/utils@8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)(vitest@2.1.8)': dependencies: '@typescript-eslint/utils': 8.17.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2) eslint: 9.17.0(jiti@2.4.2) optionalDependencies: typescript: 5.7.2 - vitest: 2.1.8(@types/node@22.10.2) + vitest: 2.1.8(@types/node@22.10.2)(@vitest/ui@2.1.8)(jsdom@25.0.1) '@vitest/expect@2.1.8': dependencies: @@ -4327,6 +4472,17 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@2.1.8(vitest@2.1.8)': + dependencies: + '@vitest/utils': 2.1.8 + fflate: 0.8.2 + flatted: 3.3.2 + pathe: 1.1.2 + sirv: 3.0.0 + tinyglobby: 0.2.10 + tinyrainbow: 1.2.0 + vitest: 2.1.8(@types/node@22.10.2)(@vitest/ui@2.1.8)(jsdom@25.0.1) + '@vitest/utils@2.1.8': dependencies: '@vitest/pretty-format': 2.1.8 @@ -4433,6 +4589,8 @@ snapshots: acorn@8.14.0: {} + agent-base@7.1.3: {} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -4717,6 +4875,10 @@ snapshots: cssesc@3.0.0: {} + cssstyle@4.1.0: + dependencies: + rrweb-cssom: 0.7.1 + csstype@3.1.3: {} cypress@13.17.0: @@ -4771,6 +4933,11 @@ snapshots: dependencies: assert-plus: 1.0.0 + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + dayjs@1.11.13: {} de-indent@1.0.2: {} @@ -4791,6 +4958,8 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + decimal.js@10.4.3: {} + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -5349,6 +5518,12 @@ snapshots: dependencies: pend: 1.2.0 + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -5502,16 +5677,38 @@ snapshots: hosted-git-info@2.8.9: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + http-signature@1.4.0: dependencies: assert-plus: 1.0.0 jsprim: 2.0.2 sshpk: 1.18.0 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + human-signals@1.1.1: {} human-signals@2.1.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -5562,6 +5759,8 @@ snapshots: is-path-inside@3.0.3: {} + is-potential-custom-element-name@1.0.1: {} + is-stream@2.0.1: {} is-text-path@2.0.0: @@ -5598,6 +5797,34 @@ snapshots: jsdoc-type-pratt-parser@4.1.0: {} + jsdom@25.0.1: + dependencies: + cssstyle: 4.1.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@0.5.0: {} jsesc@3.0.2: {} @@ -6076,6 +6303,8 @@ snapshots: pkg-types: 1.2.1 ufo: 1.5.4 + mrmime@2.0.0: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -6103,6 +6332,8 @@ snapshots: dependencies: boolbase: 1.0.0 + nwsapi@2.2.16: {} + object-inspect@1.13.3: {} once@1.4.0: @@ -6174,6 +6405,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@7.2.1: + dependencies: + entities: 4.5.0 + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -6339,6 +6574,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.28.0 fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6351,6 +6588,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -6393,6 +6634,12 @@ snapshots: simple-git-hooks@2.11.1: {} + sirv@3.0.0: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + sisteransi@1.0.5: {} slashes@3.0.12: {} @@ -6507,6 +6754,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} + synckit@0.6.2: dependencies: tslib: 2.8.1 @@ -6528,6 +6777,11 @@ snapshots: tinyexec@0.3.1: {} + tinyglobby@0.2.10: + dependencies: + fdir: 6.4.2(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} @@ -6550,10 +6804,16 @@ snapshots: dependencies: eslint-visitor-keys: 3.4.3 + totalist@3.0.1: {} + tough-cookie@5.0.0: dependencies: tldts: 6.1.65 + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-api-utils@1.4.3(typescript@5.7.2): @@ -6718,7 +6978,7 @@ snapshots: jiti: 2.4.2 yaml: 2.6.1 - vitest@2.1.8(@types/node@22.10.2): + vitest@2.1.8(@types/node@22.10.2)(@vitest/ui@2.1.8)(jsdom@25.0.1): dependencies: '@vitest/expect': 2.1.8 '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.2)) @@ -6742,6 +7002,8 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.10.2 + '@vitest/ui': 2.1.8(vitest@2.1.8) + jsdom: 25.0.1 transitivePeerDependencies: - less - lightningcss @@ -6778,6 +7040,10 @@ snapshots: optionalDependencies: typescript: 5.7.2 + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + wait-on@8.0.1(debug@4.4.0): dependencies: axios: 1.7.9(debug@4.4.0) @@ -6788,6 +7054,19 @@ snapshots: transitivePeerDependencies: - debug + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.1.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -6813,8 +7092,14 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + xml-name-validator@4.0.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} yallist@4.0.0: {} diff --git a/src/index.test.ts b/src/index.test.ts index 62bb118a..f6d427e4 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -2,13 +2,16 @@ import { apiPlugin, isRichTextEmpty, renderRichText, + type SbInitResult, + type SbPluginFactory, storyblokEditable, storyblokInit, } from '../src'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import richTextFixture from './fixtures/richTextObject.json'; import emptyRichTextFixture from './fixtures/emptyRichTextObject.json'; +import { loadBridge } from './bridge'; describe('@storyblok/js', () => { afterEach(() => { @@ -30,7 +33,7 @@ describe('@storyblok/js', () => { use: [apiPlugin], }); - const result = await storyblokApi.getAll('cdn/stories'); + const result = await storyblokApi!.getAll('cdn/stories', { version: 'draft' }); expect(result.length).toBeGreaterThan(0); }); @@ -38,8 +41,8 @@ describe('@storyblok/js', () => { it('logs an error if no access token is provided', () => { const spy = vi.spyOn(console, 'error'); storyblokInit({ - accessToken: null, - apiOptions: { accessToken: null }, + accessToken: undefined, + apiOptions: { accessToken: undefined }, use: [apiPlugin], }); @@ -49,6 +52,200 @@ describe('@storyblok/js', () => { }); }); + describe('api Plugin', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('should handle failed API calls', async () => { + // Create an isolated mock just for this test + const fetchSpy = vi.spyOn(globalThis, 'fetch') + .mockRejectedValueOnce(new Error('API Error')); + + const { storyblokApi } = storyblokInit({ + accessToken: 'test-token', + use: [apiPlugin], + }); + + await expect(storyblokApi!.get('cdn/stories/test')).rejects.toThrow(); + + // Verify the mock was called + expect(fetchSpy).toHaveBeenCalled(); + }); + + it('should support different API endpoints', async () => { + // Create a spy that returns successful responses + const fetchSpy = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(new Response(JSON.stringify({ stories: [] }))) + .mockResolvedValueOnce(new Response(JSON.stringify({ links: [] }))) + .mockResolvedValueOnce(new Response(JSON.stringify({ datasources: [] }))); + + const { storyblokApi } = storyblokInit({ + accessToken: 'test-token', + use: [apiPlugin], + }); + + await storyblokApi!.get('cdn/stories'); + await storyblokApi!.get('cdn/links'); + await storyblokApi!.get('cdn/datasources'); + + // Verify different endpoints were called + expect(fetchSpy).toHaveBeenCalledTimes(3); + expect(fetchSpy.mock.calls[0][0].toString()).toContain('cdn/stories'); + expect(fetchSpy.mock.calls[1][0].toString()).toContain('cdn/links'); + expect(fetchSpy.mock.calls[2][0].toString()).toContain('cdn/datasources'); + }); + + it('should handle pagination correctly', async () => { + // Mock fetch to return paginated responses + const fetchSpy = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(new Response(JSON.stringify({ + stories: [{ id: 1 }, { id: 2 }], + total: 4, + perPage: 2, + page: 1, + }))) + .mockResolvedValueOnce(new Response(JSON.stringify({ + stories: [{ id: 3 }, { id: 4 }], + total: 4, + perPage: 2, + page: 2, + }))); + + const { storyblokApi } = storyblokInit({ + accessToken: 'test-token', + use: [apiPlugin], + }) as { storyblokApi: any }; + + // Define type for stories + const allStories: Array<{ id: number }> = []; + let page = 1; + const perPage = 2; + + // First page + let response = await storyblokApi.get('cdn/stories', { + page, + perPage, + }); + + const total = response.data.total; + allStories.push(...response.data.stories); + + // Get remaining pages + while (allStories.length < total) { + page++; + response = await storyblokApi.get('cdn/stories', { + page, + perPage, + }); + allStories.push(...response.data.stories); + } + + // Verify pagination results + expect(allStories).toHaveLength(4); + expect(allStories).toEqual([ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + ]); + + // Verify that fetch was called twice for the two pages + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + }); + + describe('initialization', () => { + it('should initialize multiple plugins', () => { + // Create two mock plugins with proper typing + const plugin1: SbPluginFactory = () => ({ feature1: 'value1' }); + const plugin2: SbPluginFactory = () => ({ feature2: 'value2' }); + + const result = storyblokInit({ + accessToken: 'test-token', + use: [plugin1, plugin2], + }) as SbInitResult & { + feature1: string; + feature2: string; + }; + + // Verify both plugins were initialized + expect(result).toEqual({ + feature1: 'value1', + feature2: 'value2', + }); + + // Verify the result has both plugin features + expect(result.feature1).toBe('value1'); + expect(result.feature2).toBe('value2'); + }); + + it('should handle custom bridge URLs', () => { + const customBridgeUrl = 'https://custom-bridge.com/bridge.js'; + + // Mock window.location to simulate being in the editor + const originalLocation = window.location; + Object.defineProperty(window, 'location', { + value: { search: '?_storyblok_tk=123' }, + writable: true, + }); + + storyblokInit({ + accessToken: 'test-token', + bridgeUrl: customBridgeUrl, + }); + + // Get the bridge script element + const bridgeScript = document.querySelector('#storyblok-javascript-bridge'); + + // Verify bridge script was added with correct URL + expect(bridgeScript).toBeTruthy(); + expect(bridgeScript?.getAttribute('src')).toBe(customBridgeUrl); + + // Restore original window.location + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + }); + }); + + it('should handle custom bridge URL with query params', () => { + const baseUrl = 'https://custom-bridge.com/bridge.js'; + const queryParams = '?version=2&debug=true'; + const customBridgeUrl = `${baseUrl}${queryParams}`; + + // Mock window.location to simulate being in the editor + const originalLocation = window.location; + Object.defineProperty(window, 'location', { + value: { + search: '?_storyblok_tk=123', + href: 'http://localhost?_storyblok_tk=123', + }, + writable: true, + }); + + storyblokInit({ + accessToken: 'test-token', + bridge: true, + bridgeUrl: customBridgeUrl, + }); + + // Get the bridge script element + const bridgeScript = document.querySelector('#storyblok-javascript-bridge'); + + // Verify the script was created + expect(bridgeScript).toBeTruthy(); + // Verify base URL is present (browser strips query params) + expect(bridgeScript?.getAttribute('src')).toBe(baseUrl); + + // Restore original window.location + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + }); + }); + }); + describe('editable', () => { it('gets data-blok-c and data-blok-uid', async () => { const { storyblokApi } = storyblokInit({ @@ -56,7 +253,7 @@ describe('@storyblok/js', () => { use: [apiPlugin], }); - const { data } = await storyblokApi.get('cdn/stories/demo'); + const { data } = await storyblokApi!.get('cdn/stories/demo'); const blok = data.story.content; blok._editable = ``; @@ -79,16 +276,235 @@ describe('@storyblok/js', () => { }); it('should return true when passing a falsy value', () => { storyblokInit({ accessToken: 'wANpEQEsMYGOwLxwXQ76Ggtt', bridge: false }); - expect(isRichTextEmpty('')).toBe(true); + expect(isRichTextEmpty('' as any)).toBe(true); }); }); - // TODO: This might change when legacy rich text resolver is removed and - // the new rich text resolver is implemented instead - describe('legacy Rich Text Resolver', () => { - it('should return the rendered HTML when passing a valid RichText object', () => { - storyblokInit({ accessToken: 'wANpEQEsMYGOwLxwXQ76Ggtt', bridge: false }); - expect(renderRichText(richTextFixture)).toMatchSnapshot(); + describe('bridge functionality', () => { + beforeEach(() => { + // Clear DOM and reset window properties + document.head.innerHTML = ''; + window.storyblokRegisterEvent = () => {}; + vi.clearAllMocks(); + }); + + it('should load bridge script correctly', async () => { + const bridgeUrl = 'https://app.storyblok.com/f/storyblok-v2-latest.js'; + const loadPromise = loadBridge(bridgeUrl); + + // Check if script was added to DOM + const script = document.querySelector('#storyblok-javascript-bridge') as HTMLScriptElement; + expect(script).toBeTruthy(); + expect(script?.getAttribute('src')).toBe(bridgeUrl); + expect(script?.async).toBe(true); + + // Simulate script load + const event = new Event('load'); + script?.dispatchEvent(event); + + await expect(loadPromise).resolves.toBeDefined(); + }); + + it('should handle bridge loading errors', async () => { + const bridgeUrl = 'https://invalid-url.com/bridge.js'; + const loadPromise = loadBridge(bridgeUrl); + + const script = document.querySelector('#storyblok-javascript-bridge'); + + // Simulate script error + const event = new ErrorEvent('error', { error: new Error('Failed to load script') }); + script?.dispatchEvent(event); + + await expect(loadPromise).rejects.toBeDefined(); + }); + + it('should register callbacks correctly', () => { + const callback = vi.fn(); + + // Mock being in an iframe + const originalLocation = window.location; + delete (window as any).location; + window.location = { + ...originalLocation, + href: 'http://localhost', + }; + Object.defineProperty(window, 'parent', { + value: { location: { href: 'http://different-origin.com' } }, + writable: true, + }); + + loadBridge('https://app.storyblok.com/f/storyblok-v2-latest.js'); + + // Register callback + window.storyblokRegisterEvent(callback); + + // Simulate script load + const script = document.querySelector('#storyblok-javascript-bridge'); + const event = new Event('load'); + script?.dispatchEvent(event); + + expect(callback).toHaveBeenCalled(); + + // Restore original location + window.location = originalLocation; + }); + + it('should warn when not in editor mode', () => { + const consoleSpy = vi.spyOn(console, 'warn'); + const callback = vi.fn(); + + // Ensure we're not in an iframe + Object.defineProperty(window, 'parent', { + value: { location: window.location }, + writable: true, + }); + + loadBridge('https://app.storyblok.com/f/storyblok-v2-latest.js'); + window.storyblokRegisterEvent(callback); + + expect(consoleSpy).toHaveBeenCalledWith( + 'You are not in Draft Mode or in the Visual Editor.', + ); + expect(callback).not.toHaveBeenCalled(); + }); + + it('should execute callbacks in order of registration', async () => { + const executionOrder: number[] = []; + const callback1 = vi.fn(() => executionOrder.push(1)); + const callback2 = vi.fn(() => executionOrder.push(2)); + const callback3 = vi.fn(() => executionOrder.push(3)); + + // Mock being in an iframe + Object.defineProperty(window, 'parent', { + value: { location: { href: 'http://different-origin.com' } }, + writable: true, + }); + + loadBridge('https://app.storyblok.com/f/storyblok-v2-latest.js'); + + window.storyblokRegisterEvent(callback1); + window.storyblokRegisterEvent(callback2); + window.storyblokRegisterEvent(callback3); + + // Simulate script load + const script = document.querySelector('#storyblok-javascript-bridge'); + const event = new Event('load'); + script?.dispatchEvent(event); + + expect(executionOrder).toEqual([1, 2, 3]); + }); + }); + + describe('rich Text Rendering', () => { + beforeEach(() => { + // Initialize Storyblok before each test + storyblokInit({ + accessToken: 'test-token', + bridge: false, + }); + }); + + it('should handle nested content structures', () => { + const nestedContentFixture = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + text: 'Hello ', + type: 'text', + }, + { + text: 'nested', + type: 'text', + marks: [{ type: 'bold' }], + }, + { + text: ' world', + type: 'text', + }, + ], + }, + ], + }; + const rendered = renderRichText(nestedContentFixture); + expect(rendered).toMatch(/

Hello nested<\/b> world<\/p>/); + }); + + it('should render different node types correctly', () => { + const rendered = renderRichText(richTextFixture); + + // Test paragraph with bold text + expect(rendered).toContain('in bold'); + + // Test lists + expect(rendered).toContain('