From 94a0e5c164a6957a9358ff4177e950e996adbbae Mon Sep 17 00:00:00 2001 From: Brandon Marshall Date: Wed, 25 Sep 2024 09:37:52 -0700 Subject: [PATCH] MWPW-158345 Decode CaaS Config (#247) * MWPW-158345 Decode Multiple CaaS Configs from Query Index --- blocks/url-decode/readme.md | 44 ++++ blocks/url-decode/url-decode.css | 182 +++++++++++++ blocks/url-decode/url-decode.js | 239 ++++++++++++++++++ test/blocks/url-decode/mocks/query-index.json | 41 +++ test/blocks/url-decode/url-decode.test.js | 148 +++++++++++ 5 files changed, 654 insertions(+) create mode 100644 blocks/url-decode/readme.md create mode 100644 blocks/url-decode/url-decode.css create mode 100644 blocks/url-decode/url-decode.js create mode 100644 test/blocks/url-decode/mocks/query-index.json create mode 100644 test/blocks/url-decode/url-decode.test.js diff --git a/blocks/url-decode/readme.md b/blocks/url-decode/readme.md new file mode 100644 index 0000000..d2c8bf9 --- /dev/null +++ b/blocks/url-decode/readme.md @@ -0,0 +1,44 @@ +# URL Decode Validator + +This block validates that a CaaS indexed URL is able to be decoded. + +## Use Case + +Select a locale to fetch the query index from. +The block will then attempt to decode the URL and return the result in a table. + +## Process + +1. **Fetch Query Index**: The block fetches the index (`query-index.json`) based on the selected locale. +2. **Decode URLs**: The URLs from the fetched JSON are decoded using the `validateDecodedUrls` function. +3. **Validation**: Each decoded URL is validated to check if it can be successfully decoded and accessed. +4. **Generate Report**: The results are compiled into a table + +## Generate Report + +The URL Decode block will create a table with the following columns: + +- `path`: The page where the CaaS links are indexed from. +- `valid`: Status of the URL decode (true if all links are valid, false otherwise). +- `message`: Detailed message about the validation status. +- `count`: Number of URLs decoded. + +## Validation Process + +Each entry in the query index processed by parsing the caas-url column and looping through each URL. Each URL is decoded using the `parseEncodedConfig` or `decodeCompressedString` function. If any of the URLs are not able to be decoded, the validation will fail. + +## Decode URLs output + +The `decodeUrls` function is designed to decode an array of URLs or a single URL from a JSON string. It attempts to decode each URL using the `decodeUrl` function and returns an array of decoded configurations. If a URL cannot be decoded, null is returned for that URL. + +Example output: + +```json +[ + { + "decodedKey1": "decodedValue1", + "decodedKey2": "decodedValue2" + }, + null +] +``` diff --git a/blocks/url-decode/url-decode.css b/blocks/url-decode/url-decode.css new file mode 100644 index 0000000..c6581c6 --- /dev/null +++ b/blocks/url-decode/url-decode.css @@ -0,0 +1,182 @@ +.url-decode { + --color-error: #c9252d; + --color-border: #d5d5d5; + --color-hover: #F0F0F0; + --color-active: #E5E5E5; + + font-size: 14px; + padding: 20px; +} + +.url-decode .options, +.url-decode .ribbon { + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} + +.url-decode .ribbon { + border-bottom: solid 1px var(--color-border); +} + +.url-decode .options p, +.url-decode .ribbon p { + margin: 0; +} + +.url-decode .ribbon .error { + color: var(--color-error); + font-weight: bold; +} + +.url-decode .options label { + margin: 0 5px; +} + +.url-decode .table { + border: solid 1px var(--color-border); + padding: 0 10px; + max-height: 80vh; + overflow: auto; +} + +.url-decode table { + width: 100%; + border-spacing: 0; + overflow: scroll; +} + +.url-decode tr { + margin-bottom: 0; + font-size: 14px; + align-items: center; +} + +.url-decode tr th { + max-width: 25vw; + background: var(--color-white); + color: var(--color-black); + cursor: pointer; +} + +.url-decode tbody tr td { + max-width: 100vw; +} + +.url-decode thead { + position: sticky; + top: -3px; + background: var(--color-white); +} + +.url-decode tbody th { + position: sticky; + left: 0; +} + +.url-decode thead tr th { + padding: 10px 16px; + font-size: 14px; + font-weight: bold; + letter-spacing: 1px; + z-index: 1; +} + +.url-decode tbody tr th, +.url-decode tbody tr td { + font-size: 14px; + text-align: left; + vertical-align: top; + border: 1px solid var(--color-border); + background: var(--color-white); + color: #222; + padding: 14px 16px; +} + +.url-decode tr th:hover { + background: var(--color-hover); +} + +.url-decode tr th:active, +.url-decode tr th.sorted { + background: var(--color-active); +} + +.url-decode tr th.sorted::after { + position: relative; + color: #888; + margin-left: 5px; +} + +.url-decode tr th.sorted-desc::after { + content: "▼"; +} + +.url-decode tr th.sorted-asc::after { + content: "▲"; +} + +.url-decode.centered tbody tr td { + text-align: center; +} + +.url-decode thead tr th:first-child { + left: 0; + z-index: 2; +} + +.url-decode tbody tr.cta-row th, +.url-decode tbody tr.cta-row td { + border: none; +} + +.url-decode tbody tr:not(.cta-row):hover th, +.url-decode tbody tr:not(.cta-row):hover td { + background-color: var(--color-hover); +} + +.url-decode tbody tr:not(.cta-row):active th, +.url-decode tbody tr:not(.cta-row):active td { + background-color: var(--color-active); +} + +.url-decode table thead tr:first-child th:first-child { + border-top-left-radius: 5px; +} + +.url-decode table thead tr:first-child th:last-child { + border-top-right-radius: 5px; +} + +.url-decode table tbody tr:last-child th:first-child { + border-bottom-left-radius: 5px; +} + +.url-decode table tbody tr:last-child td:last-child { + border-bottom-right-radius: 5px; +} + +.url-decode svg.icon-milo { + position: initial; +} + +@media screen and (min-width: 1200px) { + .url-decode tr th { + background: transparent; + color: inherit; + } + + .url-decode tbody tr.cta-row th, + .url-decode tbody tr.cta-row td { + background: transparent; + color: inherit; + } + + .url-decode table tbody tr:first-child th:first-child { + border-top-left-radius: 5px; + } + + .url-decode table tbody tr:first-child td:last-child { + border-top-right-radius: 5px; + } +} diff --git a/blocks/url-decode/url-decode.js b/blocks/url-decode/url-decode.js new file mode 100644 index 0000000..7c978fe --- /dev/null +++ b/blocks/url-decode/url-decode.js @@ -0,0 +1,239 @@ +import { LIBS } from '../../scripts/scripts.js'; + +const { createTag, getConfig, parseEncodedConfig } = await import(`${LIBS}/utils/utils.js`); +const { decodeCompressedString } = await import(`${LIBS}/blocks/caas/utils.js`); + +const URL_COLUMN = 'caas-url'; +const DEFAULT_LOCALE = 'us'; + +/* c8 ignore next */ +const delay = (milliseconds) => new Promise((resolve) => { setTimeout(resolve, milliseconds); }); + +export const loadQueryIndex = async (url, callback = null) => { + const queryData = []; + const response = await fetch(url); + + if (!response.ok) throw new Error(`Failed to fetch data from ${url}`); + + const json = await response.json(); + const { total, offset, limit, data } = json[':type'] === 'multi-sheet' ? json.sitemap : json; + + if (!Array.isArray(data)) throw new Error(`Invalid data format: ${url}`); + + queryData.push(...data); + const remaining = total - offset - limit; + callback?.(total - remaining, total); + + /* c8 ignore next 7 */ + if (remaining > 0) { + const nextUrl = new URL(url); + nextUrl.searchParams.set('limit', limit); + nextUrl.searchParams.set('offset', offset + limit); + await delay(500); + queryData.push(...await loadQueryIndex(nextUrl.toString(), callback)); + } + + return queryData; +}; + +const sortByHeader = (data, header, invert) => { + data.sort((a, b) => a[header].toString().localeCompare(b[header].toString()) * (invert ? -1 : 1)); +}; + +export const createTable = (data, sortColumn = '', invert = false) => { + const headersRow = createTag('tr'); + const headers = Object.keys(data[0]); + + if (sortColumn && headers.includes(sortColumn)) { + sortByHeader(data, sortColumn, invert); + } + + headers.forEach((header) => { + const th = createTag('th', { scope: 'col' }, header); + const sorted = header === sortColumn; + if (sorted) { + th.classList.add('sorted'); + th.classList.add(invert ? 'sorted-desc' : 'sorted-asc'); + } + th.addEventListener('click', () => { + const table = createTable(data, header, sorted && !invert); + document.querySelector('.table').replaceWith(table); + }); + headersRow.append(th); + }); + + const thead = createTag('thead', null, headersRow); + const tbody = createTag('tbody'); + + data.forEach((row) => { + const bodyRow = createTag('tr'); + headers.forEach((header) => { + if (row[header] instanceof HTMLElement) { + bodyRow.append(createTag('td', null, row[header])); + } else { + bodyRow.append(createTag('td', null, String(row[header]))); + } + }); + tbody.append(bodyRow); + }); + + const table = createTag('table', null, [thead, tbody]); + + return createTag('div', { class: 'table' }, table); +}; + +export async function decodeUrl(url) { + const encodedConfig = url.split('#')[1]; + if (!encodedConfig) return null; + + if (encodedConfig.startsWith('~~')) { + const config = await decodeCompressedString(encodedConfig.substring(2)); + return config; + } + + const config = parseEncodedConfig(encodedConfig); + return config; +} + +async function decodeUrls(data) { + let encodedLinks = []; + + try { + const parsed = JSON.parse(data); + if (parsed) { + encodedLinks = Array.isArray(parsed) ? parsed : [encodedLinks]; + } + } catch (e) { + if (data) encodedLinks = [data]; + } + + const decodedLinks = []; + for (const link of encodedLinks) { + try { + const decodedLink = await decodeUrl(link); + decodedLinks.push(decodedLink); + } catch (e) { + decodedLinks.push(null); + } + } + + return decodedLinks; +} + +async function validateUrls(data, configColumn) { + return Promise.all(data.map(async (page) => { + const pageUrl = new URL(page.path, window.location.origin); + if (!window.location.pathname.includes('.html')) { + pageUrl.pathname = pageUrl.pathname.replace('.html', ''); + } + + const path = createTag('a', { href: pageUrl.href, target: '_blank' }, pageUrl.pathname); + const configs = await decodeUrls(page[configColumn]); + const count = configs.length; + + if (configs.length === 0) return { path, valid: true, message: 'No links Found', count }; + + for (const [i, config] of configs.entries()) { + if (!config) return { path, valid: false, message: `Could not decode link ${i + 1}`, count }; + + if (Object.keys(config).length === 0) return { path, valid: false, message: 'Empty link', count }; + } + + return { path, valid: true, message: 'Valid link(s) found', count }; + })); +} + +async function generateReport(el, configColumn) { + const report = el.querySelector('.report'); + const locale = el.querySelector('select#locale').value; + const queryIndex = new URL(`${locale}/query-index.json`, window.location.origin); + + const queryLink = createTag('a', { href: queryIndex.href, target: '_blank' }, queryIndex.href); + report.replaceChildren(createTag('p', null, ['Fetching data from ', queryLink])); + + let data = []; + + try { + data = await loadQueryIndex(queryIndex.href, (offset, total) => { + report.replaceChildren(createTag('p', null, ['Fetching data from ', queryLink, ` (${offset} of ${total})`])); + }); + } catch (e) { + /* c8 ignore next 4 */ + window.lana?.log(`Error fetching data from url: ${queryIndex.href}`, { tags: 'info,url-decode' }); + report.append(createTag('p', { class: 'error' }, 'Error fetching data')); + return; + } + + const isColumnMissing = !Object.keys(data[0]).includes(configColumn); + const decodedReports = await validateUrls(data, configColumn); + const results = decodedReports.reduce((acc, { valid, count }) => { + acc.count += count; + if (valid) { + acc.valid += 1; + } else { + acc.invalid += 1; + } + return acc; + }, { valid: 0, invalid: 0, count: 0 }); + const table = createTable(decodedReports); + const summary = createTag('p', null, `Valid Pages: ${results.valid}, Invalid Pages: ${results.invalid}, Total Link Count: ${results.count}`); + const error = createTag('p', { class: 'error' }, isColumnMissing ? 'Error: Update query index to include the "caas-url" column.' : ''); + const downloadData = encodeURIComponent(data.map((row) => Object.values(row).join(',')).join('\n')); + const download = createTag('a', { + href: `data:text/csv;charset=utf-8,${downloadData}`, + download: `data-${locale || DEFAULT_LOCALE}.csv`, + }, 'Download CSV'); + const ribbon = createTag('div', { class: 'ribbon' }, [summary, error, download]); + + report.append(ribbon, table); +} + +function onSubmit(el) { + return () => { + const url = new URL(window.location.href); + const title = document.title.split(' - ')[0]; + const locale = el.querySelector('select#locale').value || DEFAULT_LOCALE; + + url.searchParams.set('locale', locale); + document.title = `${title} - ${locale}`; + window.history.pushState({}, '', url); + generateReport(el, URL_COLUMN); + }; +} + +function updateResults(el) { + const urlParams = new URLSearchParams(window.location.search); + const queryLocale = urlParams.get('locale'); + const selectLocale = el.querySelector('select#locale'); + + /* c8 ignore next 4 */ + if (queryLocale) { + selectLocale.value = queryLocale === DEFAULT_LOCALE ? '' : queryLocale; + generateReport(el, URL_COLUMN); + } +} + +export default async function init(el) { + const config = getConfig(); + + const selectLabel = createTag('label', { for: 'locale' }, 'Select a locale:'); + const selectLocale = createTag('select', { name: 'locale', id: 'locale' }); + + for (const locale of Object.keys(config.locales)) { + const option = createTag('option', { value: locale }, locale || DEFAULT_LOCALE); + selectLocale.append(option); + } + + const locale = createTag('div', { class: 'locale' }, [selectLabel, selectLocale]); + const submit = createTag('button', { type: 'submit' }, 'Create Report'); + const options = createTag('div', { class: 'options' }, [locale, submit]); + + const report = createTag('div', { class: 'report' }); + + el.replaceChildren(options, report); + + submit.addEventListener('click', onSubmit(el)); + window.addEventListener('popstate', () => { updateResults(el); }); + + updateResults(el); +} diff --git a/test/blocks/url-decode/mocks/query-index.json b/test/blocks/url-decode/mocks/query-index.json new file mode 100644 index 0000000..1261d93 --- /dev/null +++ b/test/blocks/url-decode/mocks/query-index.json @@ -0,0 +1,41 @@ +{ + "total":6, + "offset":0, + "limit":6, + "data":[{ + "path": "/resources/2021-state-work", + "title": "The 2021 State of Work: How COVID-19 Changed Digital Work", + "date": "44715.6956838079", + "caas-url": "[\"https://milo.adobe.com/tools/caas#eyJsIjoiZW5fdXMiLCJkIjoiL3Jlc291cmNlc3VuZGVmaW5lZCIsImFyIjp0cnVlLCJ0ZXN0IjpmYWxzZSwicSI6e30sInBjIjp7IjEiOiJqcyIsIjIiOiJmYWFzX3N1Ym1pc3Npb24iLCIzIjoic2ZkYyIsIjQiOiJkZW1hbmRiYXNlIiwiNSI6IiJ9LCJwIjp7ImpzIjp7IjMyIjoidW5rbm93biIsIjM2IjoiNzAxNVkwMDAwMDF6UWthUUFFIiwiMzkiOiIiLCI3NyI6MSwiNzgiOjEsIjc5IjoxLCI5MCI6IkZBQVMiLCI5MiI6Mjg0NiwiOTMiOiIyODQ3IiwiOTQiOjMyNzMsIjE3MiI6IlN0YXRlIG9mIFdvcmsgMjAyMSAoVVMpIiwiMTczIjoiaHR0cDovL2xvY2FsaG9zdDozMDAxL3Jlc291cmNlcy8yMDIxLXN0YXRlLXdvcmsuaHRtbD9ob3N0PWh0dHBzJTNhJTJmJTJmYnVzaW5lc3MuYWRvYmUuY29tIiwiMTc2IjoiaHR0cDovL2xvY2FsaG9zdDozMDAxL3Jlc291cmNlcy8yMDIxLXN0YXRlLXdvcmsvdGhhbmsteW91Lmh0bWwiLCIxNzgiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEvdG9vbHMvaW1wb3J0ZXIvaGVsaXgtaW1wb3J0ZXItdWkvaW1wb3J0LWJ1bGsuaHRtbCJ9LCJmYWFzX3N1Ym1pc3Npb24iOnt9LCJzZmRjIjp7ImNvbnRhY3RJZCI6bnVsbH0sImRlbWFuZGJhc2UiOnt9fSwiYXMiOiJ0cnVlIiwibyI6e30sImUiOnt9LCJjb29raWUiOnsicCI6eyJqcyI6e319fSwidXJsIjp7InAiOnsianMiOnsiMzYiOiI3MDEzMDAwMDAwMGtZZTBBQUUifX19LCJqcyI6eyJpZCI6IjYzIiwibCI6ImVuX3VzIiwiZCI6Ii9yZXNvdXJjZXMvMjAyMS1zdGF0ZS13b3JrL3RoYW5rLXlvdS5odG1sIiwiYXMiOiJ0cnVlIiwiYXIiOnRydWUsInBjIjp7IjEiOiJqcyIsIjIiOiJmYWFzX3N1Ym1pc3Npb24iLCIzIjoic2ZkYyIsIjQiOiJkZW1hbmRiYXNlIiwiNSI6IiJ9LCJxIjp7fSwicCI6eyJqcyI6eyIzNiI6IjcwMTVZMDAwMDAxelFrYVFBRSIsIjM5IjoiIiwiNzciOjEsIjc4IjoxLCI3OSI6MSwiOTAiOiJGQUFTIiwiOTIiOjI4NDYsIjkzIjoiMjg0NyIsIjk0IjozMjczLCIxNzIiOiJTdGF0ZSBvZiBXb3JrIDIwMjEgKFVTKSJ9fSwiZSI6e319LCJvbmV0cnVzdF9hZHZlcnRpc2luZ19hY2NlcHRhbmNlIjoibm8iLCJvbmV0cnVzdF9wZXJmb3JtYW5jZV9hY2NlcHRhbmNlIjoibm8iLCJvbmV0cnVzdF9mdW5jdGlvbmFsaXR5X2FjY2VwdGFuY2UiOiJubyIsImNsZWFyYml0U3RlcCI6MSwiZm9ybVRhZyI6ImZhYXMtUkZJIiwiaWQiOiI2MyIsIl9mYyI6MSwiY29tcGxldGUiOnRydWUsInN0eWxlX2xheW91dCI6ImNvbHVtbjIiLCJjbGVhYml0U3R5bGUiOiIifQ==\"]" + }, + { + "path": "/resources/event-sessions/turn-newbies-into-superfans-building-an-epic-learning-academy", + "title": "Build an Epic Learning Academy", + "date": "45470", + "caas-url": "[\"/tools/caas#~~H4sIAAAAAAAACq1WTY8bNwz9Lzp74s1u24Nv+4kacLLb2DkVxoKW6BliNeKU4tjrBPnvhebDYztToEB7MaxHSqTIx6f5biCAPyjZeM/eo1Xi8BlKNDNjJoNxJWDf5mUlGCNxMLMt+IjJwS04J7uCPJrZn98NBRVYQd6gZmaevzTHuM7BWIA4sxwUg2Z6qHAKomQ9msmIzdZRuUTJorIcRl0cljxqwA3z26glr8mNx6Ow5VygKsiO2gUrFh017cjheCJ73FAAGbcVpJhVUOG4vRJ2tdVMuR532HjOzfrH5F9WvjsvTj2CBAp5VkKAHMWsf6wnJpWsBHmbWw5LTHRoeXCKfw3xxGJB3FIPPhHm4+w6IUce3WnoTZVQCW0HL2mmtYQs4H5DGDMKylmsK5QthJhtavIuZQkhw4psdkwbLDgszw9c0rcmjaurBg4KFFDMzNxcfwLJKXQwBl0dKuwKs05gHVQOZtYXuFlO62gmxvFn1gV8OywY3JH3jiJsPN5BCChxgAW2+rA5rjG4iimkWu33+w/geIMfLJdTW1CJAhlUNB0uYNKOHQmHEkNXYHy3vnbo7kFcl24HDflvwfsN2LfHIZyZmC2C1jLs/N5ffu7MzBSqVZxNp5s6UsAYT5ITjFyLxTjFXSJabIc+ThuX7JI6maModaW0wwyCy9BjDomfaIsPhZbeJHr+19hWELTpfOb5AD47asMWwgYiZnvSIlMBSsn9f4HxvfIsJ9emkGmBGb5XKIRBCXwGOWZb4TKrIMZUCOWMyhIlLbpcUqPIK8rj7tjdFliwhab/M+Nxqyd4O8osR6jveLtYFrx/LCs9HBmX16oN5395NxND4YIpHkJeQ4491fv1FBP5PBy4bmYjDU1dJYjBfWLB0Vnm4A8phbtOHo5c65JhOX0c1hNTQU6hueptoLL5czwVcnTm1OUxpBEbZm6w/FFDUNImdhixdzcIHPDsxK8RVwWWeDPs8WCxYO9QvopveyIYa6/xBeWlrdSNmZiIILZ4IvT9FEbUdNs7Fte0RaXGiYkn5YhPTZeOwc5sz+G8Vsn41Lf4BFs2gc+gFSv4L22Wg4FFH0DxNtqfsAe8BHELtU8cFAiOS9PCbcVfuKqrM/cW/9K4LqGsPIX83CExepmexlP0qROgM7A95RzCiLJjkhdmb2Yfr66uLgxNUDSzmxZfkfqfLtqAFzdNA53evg1YLs16YhTy2Db6n/U4+ZjkKjnqrVXaURow495fjyLxen11ffO6I9y3P4pRj3suiauJc2m2KS96x9TChgArTixOz9av6dmqIy6S2wrfh2LWEZ93KB4OCwpv8RSXedhyS0hwjhLLEzf+qjHqCwiU/RMH4tqyWZuUbUOe9LDAHXoz+21iWjVNGXXvusKt7TTpNaLfmoFOn9jRlk4vmA7+HSE91d2Rprg5/xaoVXlEQqBWvm/f2wUkVrVjNGxskm5zSvH72Jfd7/EzArQqeZc+Il4gNGmleEl47KCo983V2zIV5PBeYd4PeYHgjqLbGQc5c6hAPqZePVddsVw3WhMTWC/kL6Ll4EAOI9CyJ+u6HfL5iTakwKnw86AoO/BnatCIELgcL7SEWVEeKH2X9hL0429edz6j7wsAAA==/resources/main\"]" + }, + { + "path": "/resources/event-sessions/explore-learning-in-the-experiential-age-from-passive-to-immersive", + "title": "Learning in the Experiential Age: From Passive to Immersive", + "date": "45470", + "caas-url": "https://main--milo--adobecom.hlx.page/tools/caas#~~H4sIAAAAAAAAE3VVTY/bNhD9Lzw7Wafb9qCb7V0jBrxZN/aihyIoxuRIIkxxVHJor1L0vxek9ek4N+kNOR9v3gz/FaCUZk0WzFf8J6DnHTiovMj++jYTqrFQabk6LNbktvqMz2e07EWWg/E4E2DBNKylX5ExKKOfL1ChyIQYGQ8O5GlT1Q6912RHt9WWCi0PULTxIDCtKFh2zRZs0Z88Ep0qcKeNJLvHGOkaYoy/WT+ySHBqz42JuZRg8g8l6qLk1nLQbHAhZUzoqI3mZotnNCL7fSZkX8qSbeeidroC14ixeah0wJLjW3Cvv/eYZdAWncjEp1/m8xd4/1MrLlsTWj40NQ58yCsXIhMSwGft70Pw8ULwTNUKnGp9MyxSOJGJvz2aXMyEoi/EW/jebAlUz6bSHo4Gl2AtuqGZykHOT8f+H62qSdtI6OVy+QiKjvhRUvUgS12hgw9Q64ehShFvnLUjW6Ftu4Dv0gSFKmbZltRCQ405GHMEeXoewomZyBE4uMnNXBtGlxTYnknAMmijdmBj+5KAKmAte/OWJLSsGMx5hBdaikyQ66FJHL9K/E6gfUmX56rmpueoCMypm7++i5koEVTvptQKVwwbNf0fCo/AEzBuLKM7g+mdantDkQFbBCiw00H3/4CRdQMNhSScmEeoI0SgXsjhXQVb4puxI2uaWNuyHaee9jYhcjcXaii0TbQurK7SRx8HClRifOTZRrUN8hssfwSwrDnFtnfsbU0tkDo1GN88Hkqs8HG4aEBiSUahe3PmqhGHPhj2O3S7ROBvM+ERnCzXGk3XGo+SrALXDBX20J6Ck9iBHIlZklPjwfElXRIOqsApvCZidE/6rFWUycjSce3XSVv3ba922ojkstPqCNvcnNmnEifQgTiu+MTGYCDHUYILL3/AnvAO+EJK53rUzGTAHIKJQ6m6e1fDtfM7qkM9uXDFv4JVVO2hqo0e7fp0IA75nhxP0HW7EyZgl9FtCR3+QxnXsFMIPbozabcjMiL7NJ/PbwwpSxTZ4xW/vh83jhN4E67VjijhcvLi20wwFP4qzp/v1HhGxKOuQI47/azjyhE9djtRHOcgLrj4xKGPS04hgzb+gO/8Wrf7T7V9mgmOqX5GUNoW7csnysdoiCpJmjtQnMpIxkwEj9voOnrrgwaPr2d0BpqttqdBU6P3MzDTnQUUPLqNzSkO1X//AxGj3OyCCAAA" + }, + { + "path": "/resources/event-sessions/adobe-learning-manager-disruptive-and-elegant-tech", + "title": "Learning Manager: Personalized, data-driven learning tech.", + "date": "45470", + "caas-url": "0" + }, + { + "path": "/au/customer-success-stories/charles-schwab-case-study.html", + "lastModified": "1726608639", + "robots": "noodp", + "caas-url": "[]" + }, + { + "path": "/customer-success-stories", + "title": "Customer Success Stories", + "lastModified": "1726608639" + }], + ":type":"sheet" +} diff --git a/test/blocks/url-decode/url-decode.test.js b/test/blocks/url-decode/url-decode.test.js new file mode 100644 index 0000000..18a2305 --- /dev/null +++ b/test/blocks/url-decode/url-decode.test.js @@ -0,0 +1,148 @@ +import { expect } from '@esm-bundle/chai'; +import { readFile } from '@web/test-runner-commands'; +import sinon from 'sinon'; +import init, { loadQueryIndex, createTable, decodeUrl } from '../../../blocks/url-decode/url-decode.js'; +import waitForElement from '../../helpers/waitForElement.js'; + +import { LIBS } from '../../../scripts/scripts.js'; + +const { utf8ToB64 } = await import(`${LIBS}/utils/utils.js`); +const queryIndex = await readFile({ path: './mocks/query-index.json' }); +const getCell = (row, index) => row.querySelector(`td:nth-child(${index})`).textContent; + +window.lana = { log: () => { } }; + +const QUERY_INEDX_LENGTH = 6; + +describe('URL Decode', () => { + before(() => { + sinon.stub(window.lana, 'log'); + sinon.stub(window, 'fetch').callsFake(() => Promise.resolve({ ok: true, json: () => JSON.parse(queryIndex) })); + }); + + after(() => { + sinon.restore(); + }); + + beforeEach(async () => { + const url = new URL(window.location); + url.searchParams.delete('locale'); + window.history.replaceState({}, '', url); + document.body.innerHTML = '
'; + + const el = document.querySelector('.url-decode'); + + await init(el); + }); + + it('shows list of locales', async () => { + const el = document.querySelector('.url-decode'); + const select = el.querySelector('select#locale'); + const options = select.querySelectorAll('option'); + expect(select).to.exist; + expect(options).to.have.length.greaterThan(2); + }); + + it('shows "Fetch Index" button', async () => { + const el = document.querySelector('.url-decode'); + const button = el.querySelector('button[type="submit"]'); + expect(button).to.exist; + }); + + it('shows report section', async () => { + const el = document.querySelector('.url-decode'); + const report = el.querySelector('.report'); + expect(report).to.exist; + }); + + it('fetches query index', async () => { + const data = await loadQueryIndex('https://business.adobe.com/query-index.json'); + + expect(data.length).to.equal(QUERY_INEDX_LENGTH); + }); + + it('creates a table', async () => { + const table = createTable([ + { path: 'path1', validLink: 'Yes' }, + { path: 'path2', validLink: 'No' }, + ]); + + expect(table).to.exist; + expect(table.querySelectorAll('tr')).to.have.length(3); + }); + + it('decodes base64 url', async () => { + const url = `/tools/test#${utf8ToB64(JSON.stringify({ test: 'test' }))}`; + const data = await decodeUrl(url); + + expect(data).to.deep.equal({ test: 'test' }); + }); + + it('decodes compressed URL', async () => { + const url = 'https://main--milo--adobecom.hlx.page/tools/caas#~~H4sIAAAAAAAAE3VVTY/bNhD9Lzw7Wafb9qCb7V0jBrxZN/aihyIoxuRIIkxxVHJor1L0vxek9ek4N+kNOR9v3gz/FaCUZk0WzFf8J6DnHTiovMj++jYTqrFQabk6LNbktvqMz2e07EWWg/E4E2DBNKylX5ExKKOfL1ChyIQYGQ8O5GlT1Q6912RHt9WWCi0PULTxIDCtKFh2zRZs0Z88Ep0qcKeNJLvHGOkaYoy/WT+ySHBqz42JuZRg8g8l6qLk1nLQbHAhZUzoqI3mZotnNCL7fSZkX8qSbeeidroC14ixeah0wJLjW3Cvv/eYZdAWncjEp1/m8xd4/1MrLlsTWj40NQ58yCsXIhMSwGft70Pw8ULwTNUKnGp9MyxSOJGJvz2aXMyEoi/EW/jebAlUz6bSHo4Gl2AtuqGZykHOT8f+H62qSdtI6OVy+QiKjvhRUvUgS12hgw9Q64ehShFvnLUjW6Ftu4Dv0gSFKmbZltRCQ405GHMEeXoewomZyBE4uMnNXBtGlxTYnknAMmijdmBj+5KAKmAte/OWJLSsGMx5hBdaikyQ66FJHL9K/E6gfUmX56rmpueoCMypm7++i5koEVTvptQKVwwbNf0fCo/AEzBuLKM7g+mdantDkQFbBCiw00H3/4CRdQMNhSScmEeoI0SgXsjhXQVb4puxI2uaWNuyHaee9jYhcjcXaii0TbQurK7SRx8HClRifOTZRrUN8hssfwSwrDnFtnfsbU0tkDo1GN88Hkqs8HG4aEBiSUahe3PmqhGHPhj2O3S7ROBvM+ERnCzXGk3XGo+SrALXDBX20J6Ck9iBHIlZklPjwfElXRIOqsApvCZidE/6rFWUycjSce3XSVv3ba922ojkstPqCNvcnNmnEifQgTiu+MTGYCDHUYILL3/AnvAO+EJK53rUzGTAHIKJQ6m6e1fDtfM7qkM9uXDFv4JVVO2hqo0e7fp0IA75nhxP0HW7EyZgl9FtCR3+QxnXsFMIPbozabcjMiL7NJ/PbwwpSxTZ4xW/vh83jhN4E67VjijhcvLi20wwFP4qzp/v1HhGxKOuQI47/azjyhE9djtRHOcgLrj4xKGPS04hgzb+gO/8Wrf7T7V9mgmOqX5GUNoW7csnysdoiCpJmjtQnMpIxkwEj9voOnrrgwaPr2d0BpqttqdBU6P3MzDTnQUUPLqNzSkO1X//AxGj3OyCCAAA'; + const data = await decodeUrl(url); + + expect(data).to.not.be.empty; + expect(data).to.have.property('tagsUrl', 'www.adobe.com/chimera-api/tags'); + }); + + it('shows report data', async () => { + const el = document.querySelector('.url-decode'); + const submit = el.querySelectorAll('button[type="submit"]'); + + expect(submit).to.have.length(1); + submit[0].click(); + + const table = await waitForElement('table'); + expect(table).to.exist; + + expect(window.location.search).to.include('locale=us'); + const rows = table.querySelectorAll('tr'); + + expect(rows).to.have.length(QUERY_INEDX_LENGTH + 1); + expect(rows[0].querySelector('th').textContent).to.equal('path'); + expect(rows[0].querySelector('th:nth-child(2)').textContent).to.equal('valid'); + expect(rows[0].querySelector('th:nth-child(3)').textContent).to.equal('message'); + expect(rows[0].querySelector('th:nth-child(4)').textContent).to.equal('count'); + + expect(getCell(rows[1], 3)).to.equal('Valid link(s) found'); + expect(getCell(rows[1], 4)).to.equal('1'); + expect(getCell(rows[2], 3)).to.equal('Could not decode link 1'); + expect(getCell(rows[2], 4)).to.equal('1'); + expect(getCell(rows[3], 3)).to.equal('Valid link(s) found'); + expect(getCell(rows[3], 4)).to.equal('1'); + expect(getCell(rows[4], 3)).to.equal('No links Found'); + expect(getCell(rows[4], 4)).to.equal('0'); + expect(getCell(rows[5], 3)).to.equal('No links Found'); + expect(getCell(rows[5], 4)).to.equal('0'); + expect(getCell(rows[6], 3)).to.equal('No links Found'); + expect(getCell(rows[6], 4)).to.equal('0'); + }); + + it('sorts report data', async () => { + const el = document.querySelector('.url-decode'); + const submit = el.querySelectorAll('button[type="submit"]'); + + expect(submit).to.have.length(1); + submit[0].click(); + + const table = await waitForElement('table'); + expect(table).to.exist; + + const header = el.querySelector('th:nth-child(3)'); + header.click(); + + const sortedRows = el.querySelectorAll('table tr'); + + expect(getCell(sortedRows[1], 3)).to.equal('Could not decode link 1'); + expect(getCell(sortedRows[2], 3)).to.equal('No links Found'); + + const sortHeader = el.querySelector('th:nth-child(3)'); + sortHeader.click(); + + const reversedRows = el.querySelectorAll('table tr'); + + expect(getCell(reversedRows[1], 3)).to.equal('Valid link(s) found'); + expect(getCell(reversedRows[2], 3)).to.equal('Valid link(s) found'); + }); +});