diff --git a/README.md b/README.md index fa80345a7..efd5b1f4b 100644 --- a/README.md +++ b/README.md @@ -484,10 +484,9 @@ and open needed HTML file from `tests/dist` in your browser with devtools ## How to update wiki -``` -yarn wiki:update -``` -`about-scriptlets.md` and `about-redirects.md` are being built from JSDoc notation of corresponding scriptlets and redirects source files with `@scriptlet/@redirect` and `@description` tags. +There are two scripts to update wiki: +1. `yarn wiki:build-table` — checks compatibility data updates and updates the compatibility table. Should be run manually while the release preparation. +2. `yarn wiki:build-docs` — updates wiki pages `about-scriptlets.md` and `about-redirects.md`. They are being generated from JSDoc-type comments of corresponding scriptlets and redirects source files due to `@scriptlet`/`@redirect` and `@description` tags. Runs automatically while the release build. ## Browser Compatibility | Chrome | Edge | Firefox | IE | Opera | Safari | diff --git a/bamboo-specs/build.yaml b/bamboo-specs/build.yaml index 9af2e2f4b..4bd6c8f32 100644 --- a/bamboo-specs/build.yaml +++ b/bamboo-specs/build.yaml @@ -35,6 +35,8 @@ Build: yarn install ${bamboo.varsYarn} yarn build + yarn wiki:build-docs + rm -rf node_modules - inject-variables: file: dist/build.txt diff --git a/package.json b/package.json index 81f2c59a0..39e12ca33 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,8 @@ "gui-test": "babel-node scripts/build-tests.js && open http://localhost:8585 && node ./tests/server.js", "lint": "eslint .", "lint-staged": "lint-staged", + "wiki:build-table": "node ./scripts/check-sources-updates.js && node ./scripts/build-compatibility-table.js", "wiki:build-docs": "node scripts/build-docs.js", - "wiki:check-updates": "node ./scripts/check-sources-updates.js", - "wiki:update": "yarn wiki:check-updates && node ./scripts/build-compatibility-table.js", "prepublishOnly": "yarn build", "increment": "yarn version --patch --no-git-tag-version" }, diff --git a/scripts/build-compatibility-table.js b/scripts/build-compatibility-table.js index 4165352a3..f1ab4179c 100644 --- a/scripts/build-compatibility-table.js +++ b/scripts/build-compatibility-table.js @@ -1,80 +1,115 @@ +const path = require('path'); const fs = require('fs'); -const os = require('os'); +const { EOL } = require('os'); + const { REMOVED_MARKER, + WIKI_DIR_PATH, COMPATIBILITY_TABLE_DATA_PATH, - WIKI_COMPATIBILITY_TABLE_PATH, } = require('./constants'); +const COMPATIBILITY_TABLE_OUTPUT_FILENAME = 'compatibility-table.md'; + +/** + * Path to **output** wiki compatibility table file + */ +const WIKI_COMPATIBILITY_TABLE_PATH = path.resolve( + __dirname, + WIKI_DIR_PATH, + COMPATIBILITY_TABLE_OUTPUT_FILENAME, +); + +/** + * @typedef {Object} CompatibilityItem + * @property {string} adg + * @property {string} abp + * @property {string} ubo + */ + /** - * Returns data for compatibility tables + * @typedef {Object} CompatibilityData + * @property {CompatibilityItem[]} scriptlets + * @property {CompatibilityItem[]} redirects */ -function getTableData() { + +/** + * Returns data for compatibility table + * + * @returns {CompatibilityData} input compatibility data from json + */ +const getTableData = () => { const rawData = fs.readFileSync(COMPATIBILITY_TABLE_DATA_PATH); - const parsed = JSON.parse(rawData); - return parsed; -} + return JSON.parse(rawData); +}; /** * Returns markdown row of compatibility table - * @param {{ - * adg: string, - * ubo: string, - * abp: string - * }} item { an } + * + * @param {'scriptlets'|'redirects'} id + * @param {CompatibilityItem} item params object + * @param {string} item.adg AdGuard name + * @param {string} item.abp Adblock Plus name + * @param {string} item.ubo uBlock name + * + * @returns {string} markdown table row */ -const getRow = (id, item) => { +const getRow = (id, { adg, abp, ubo }) => { let adgCell = ''; - if (item.adg) { - adgCell = item.adg.includes(REMOVED_MARKER) - ? item.adg - : `[${item.adg}](../wiki/about-${id}.md#${item.adg})`; + if (adg) { + adgCell = adg.includes(REMOVED_MARKER) + ? adg + : `[${adg}](${WIKI_DIR_PATH}/about-${id}.md#${adg})`; } - return `| ${adgCell} | ${item.ubo || ''} | ${item.abp || ''} |${os.EOL}`; + return `| ${adgCell} | ${ubo || ''} | ${abp || ''} |${EOL}`; }; /** * Generates table header + * + * @returns {string} */ const getTableHeader = () => { - let res = `| AdGuard | uBO | Adblock Plus |${os.EOL}`; - res += `|---|---|---|${os.EOL}`; + let res = `| AdGuard | uBO | Adblock Plus |${EOL}`; + res += `|---|---|---|${EOL}`; return res; }; /** - * Builds markdown string with scriptlets compatibility table - * @param {Array} data array with scriptlets names + * Builds markdown string of scriptlets/redirect compatibility table + * @param {string} title title for scriptlets or redirects + * @param {CompatibilityItem[]} data array of scriptlets or redirects compatibility data items + * @param {'scriptlets'|'redirects'} id + * + * @returns {string} scriptlets or redirects compatibility table */ -function buildTable(title, data = [], id = '') { +const buildTable = (title, data = [], id = '') => { // title - let res = `# ${title}${os.EOL}${os.EOL}`; + let res = `# ${title}${EOL}${EOL}`; // header res += getTableHeader(); // rows res += data - .map((item) => { - const row = getRow(id, item); - return row; - }) + .map((item) => getRow(id, item)) .join(''); return res; -} +}; /** - * Save tables to compatibility table + * Saves tables to compatibility table + * + * @param {string[]} args */ -function saveTables(...args) { - const res = args.join(`${os.EOL}${os.EOL}`); +const saveTables = (...args) => { + const res = args.join(`${EOL}${EOL}`); fs.writeFileSync(WIKI_COMPATIBILITY_TABLE_PATH, res); -} +}; /** - * Entry function + * Builds full compatibility table */ -function init() { +const buildCompatibilityTable = () => { const { scriptlets, redirects } = getTableData(); const scriptletsTable = buildTable( @@ -89,6 +124,6 @@ function init() { ); saveTables(scriptletsTable, redirectsTable); -} +}; -init(); +buildCompatibilityTable(); diff --git a/scripts/build-docs.js b/scripts/build-docs.js index 922037326..ab67e62a5 100644 --- a/scripts/build-docs.js +++ b/scripts/build-docs.js @@ -1,138 +1,92 @@ -const dox = require('dox'); const fs = require('fs'); const path = require('path'); - const yaml = require('js-yaml'); +const { EOL } = require('os'); -const SCRIPTLETS_FILES_DIRECTORY = '../src/scriptlets'; -const REDIRECTS_FILES_DIRECTORY = '../src/redirects'; -const STATIC_REDIRECTS = '../src/redirects/static-redirects.yml'; -const BLOCKING_REDIRECTS = '../src/redirects/blocking-redirects.yml'; - -const ABOUT_SCRIPTLETS_PATH = path.resolve(__dirname, '../wiki/about-scriptlets.md'); -const ABOUT_REDIRECTS_PATH = path.resolve(__dirname, '../wiki/about-redirects.md'); - -// files which are not scriptlets or redirects in their directories -const NON_SCRIPTLETS_FILES = [ - 'index.js', - 'scriptlets.js', - 'scriptlets-list.js', - 'scriptlets-wrapper.js', - 'scriptlets-umd-wrapper.js', -]; -const NON_REDIRECTS_FILES = [ - 'index.js', - 'redirects.js', - 'redirects-list.js', -]; +const { getDataFromFiles } = require('./helpers'); -/** - * Gets list of files - * @param {string} dirPath path to directory - */ -const getFilesList = (dirPath) => { - const filesList = fs.readdirSync(path.resolve(__dirname, dirPath), { encoding: 'utf8' }) - .filter((el) => el.includes('.js')); - return filesList; -}; +const { + WIKI_DIR_PATH, + scriptletsFilenames, + redirectsFilenames, + SCRIPTLETS_SRC_RELATIVE_DIR_PATH, + REDIRECTS_SRC_RELATIVE_DIR_PATH, +} = require('./constants'); -const scriptletsFilesList = getFilesList(SCRIPTLETS_FILES_DIRECTORY) - .filter((el) => !NON_SCRIPTLETS_FILES.includes(el)); +const STATIC_REDIRECTS_FILENAME = 'static-redirects.yml'; +const BLOCKING_REDIRECTS_FILENAME = 'blocking-redirects.yml'; -const redirectsFilesList = getFilesList(REDIRECTS_FILES_DIRECTORY) - .filter((el) => !NON_REDIRECTS_FILES.includes(el)); +// eslint-disable-next-line max-len +const STATIC_REDIRECTS_RELATIVE_SOURCE = `${REDIRECTS_SRC_RELATIVE_DIR_PATH}/${STATIC_REDIRECTS_FILENAME}`; -/** - * Gets required comments from file. - * In one file might be comments describing scriptlet and redirect as well. - * @param {string} srcPath path to file - */ -const getComments = (srcPath) => { - const srcCode = fs.readFileSync(srcPath, { encoding: 'utf8' }); - const parsedCommentsFromFile = dox.parseComments(srcCode); - const describingComment = Object.values(parsedCommentsFromFile) - .filter((comment) => { - const [base] = comment.tags; - const isNeededComment = (base - && (base.type === 'scriptlet' || base.type === 'redirect')); - return isNeededComment; - }); - - if (describingComment.length === 0) { - throw new Error(`No description in ${srcPath}. -Please add one OR edit the list of NON_SCRIPTLETS_FILES / NON_REDIRECTS_FILES.`); - } +const staticRedirectsPath = path.resolve(__dirname, STATIC_REDIRECTS_RELATIVE_SOURCE); - return describingComment; -}; +const blockingRedirectsPath = path.resolve( + __dirname, + REDIRECTS_SRC_RELATIVE_DIR_PATH, + BLOCKING_REDIRECTS_FILENAME, +); -/** - * Convert parsed comments to objects - * @param {object} requiredComments parsed comments for one file - * @param {string} sourcePath path to file - */ -const prepareData = (requiredComments, sourcePath) => { - return requiredComments.map((el) => { - const [base, sup] = el.tags; - return { - type: base.type, - name: base.string, - description: sup.string, - source: sourcePath, - }; - }); -}; +const ABOUT_SCRIPTLETS_FILENAME = 'about-scriptlets.md'; +const ABOUT_REDIRECTS_FILENAME = 'about-redirects.md'; -/** - * Gets data objects which describe every required comment in one directory - * @param {array} filesList list of files in directory - * @param {string} directoryPath path to directory - */ -const getDataFromFiles = (filesList, directoryPath) => { - const pathToDir = path.resolve(__dirname, directoryPath); - return filesList.map((file) => { - const pathToFile = path.resolve(pathToDir, file); - const requiredComments = getComments(pathToFile); - - return prepareData(requiredComments, `${directoryPath}/${file}`); - }); -}; +const aboutScriptletsPath = path.resolve(__dirname, WIKI_DIR_PATH, ABOUT_SCRIPTLETS_FILENAME); +const aboutRedirectsPath = path.resolve(__dirname, WIKI_DIR_PATH, ABOUT_REDIRECTS_FILENAME); /** * Collects required comments from files and * returns describing object for scriptlets and redirects */ const manageDataFromFiles = () => { - // eslint-disable-next-line max-len - const dataFromScriptletsFiles = getDataFromFiles(scriptletsFilesList, SCRIPTLETS_FILES_DIRECTORY); - const dataFromRedirectsFiles = getDataFromFiles(redirectsFilesList, REDIRECTS_FILES_DIRECTORY); + const dataFromScriptletsFiles = getDataFromFiles( + scriptletsFilenames, + SCRIPTLETS_SRC_RELATIVE_DIR_PATH, + ); + const dataFromRedirectsFiles = getDataFromFiles( + redirectsFilenames, + REDIRECTS_SRC_RELATIVE_DIR_PATH, + ); const fullData = dataFromScriptletsFiles.concat(dataFromRedirectsFiles).flat(Infinity); - const scriptletsData = fullData.filter((el) => { - return el.type === 'scriptlet'; - }); - const redirectsData = fullData.filter((el) => { - return el.type === 'redirect'; - }); + const scriptletsData = fullData.filter(({ type }) => type === 'scriptlet'); + const redirectsData = fullData.filter(({ type }) => type === 'redirect'); return { scriptletsData, redirectsData }; }; /** - * Generates markdown list and describing text - * @param {object} data array of filtered objects - scriptlets or redirects + * @typedef { import('./helpers').DescribingCommentData } DescribingCommentData */ -const generateMD = (data) => { - const output = data.reduce((acc, el) => { - acc.list.push(`* [${el.name}](#${el.name})\n`); - const typeOfSrc = el.type === 'scriptlet' ? 'Scriptlet' : 'Redirect'; +/** + * @typedef {Object} MarkdownData + * @property {string} list table of content + * @property {string} body main content which + */ - const body = `### ⚡️ ${el.name} -${el.description}\n -[${typeOfSrc} source](${el.source}) -* * *\n\n`; +/** + * Generates markdown list and describing text. + * + * @param {DescribingCommentData[]} dataItems array of comment data objects + * + * @returns {MarkdownData} + */ +const getMarkdownData = (dataItems) => { + const output = dataItems.reduce((acc, { + name, + type, + description, + source, + }) => { + acc.list.push(`* [${name}](#${name})${EOL}`); + + const typeOfSrc = type === 'scriptlet' ? 'Scriptlet' : 'Redirect'; + + const body = `### ⚡️ ${name} +${description}${EOL} +[${typeOfSrc} source](${source}) +* * *${EOL}${EOL}`; acc.body.push(body); return acc; @@ -146,22 +100,24 @@ ${el.description}\n /** * Generates markdown list and describing text for static redirect resources + * + * @returns {MarkdownData} */ -const mdForStaticRedirects = () => { - const staticRedirects = fs.readFileSync(path.resolve(__dirname, STATIC_REDIRECTS), { encoding: 'utf8' }); +const getMarkdownDataForStaticRedirects = () => { + const staticRedirects = fs.readFileSync(path.resolve(__dirname, staticRedirectsPath), { encoding: 'utf8' }); const parsedStaticRedirects = yaml.safeLoad(staticRedirects); - const output = parsedStaticRedirects.reduce((acc, el) => { - if (el.description) { - acc.list.push(`* [${el.title}](#${el.title})\n`); + const output = parsedStaticRedirects.reduce((acc, { title, description }) => { + if (description) { + acc.list.push(`* [${title}](#${title})${EOL}`); - const body = `### ⚡️ ${el.title} -${el.description} -[Redirect source](${STATIC_REDIRECTS}) -* * *\n\n`; + const body = `### ⚡️ ${title} +${description} +[Redirect source](${STATIC_REDIRECTS_RELATIVE_SOURCE}) +* * *${EOL}${EOL}`; acc.body.push(body); } else { - throw new Error(`No description for ${el.title}`); + throw new Error(`No description for ${title}`); } return acc; @@ -175,24 +131,28 @@ ${el.description} /** * Generates markdown list and describing text for blocking redirect resources, i.e click2load.html + * + * @returns {MarkdownData} */ -const mdForBlockingRedirects = () => { - const BLOCKING_REDIRECTS_SOURCE_DIR = '../src/redirects/blocking-redirects'; +const getMarkdownDataForBlockingRedirects = () => { + const BLOCKING_REDIRECTS_SOURCE_SUB_DIR = 'blocking-redirects'; + // eslint-disable-next-line max-len + const BLOCKING_REDIRECTS_RELATIVE_SOURCE = `${REDIRECTS_SRC_RELATIVE_DIR_PATH}/${BLOCKING_REDIRECTS_SOURCE_SUB_DIR}`; - const blockingRedirects = fs.readFileSync(path.resolve(__dirname, BLOCKING_REDIRECTS), { encoding: 'utf8' }); + const blockingRedirects = fs.readFileSync(blockingRedirectsPath, { encoding: 'utf8' }); const parsedBlockingRedirects = yaml.safeLoad(blockingRedirects); - const output = parsedBlockingRedirects.reduce((acc, el) => { - if (el.description) { - acc.list.push(`* [${el.title}](#${el.title})\n`); + const output = parsedBlockingRedirects.reduce((acc, { title, description }) => { + if (description) { + acc.list.push(`* [${title}](#${title})${EOL}`); - const body = `### ⚡️ ${el.title} -${el.description} -[Redirect source](${BLOCKING_REDIRECTS_SOURCE_DIR}/${el.title}) -* * *\n\n`; + const body = `### ⚡️ ${title} +${description} +[Redirect source](${BLOCKING_REDIRECTS_RELATIVE_SOURCE}/${title}) +* * *${EOL}${EOL}`; acc.body.push(body); } else { - throw new Error(`No description for ${el.title}`); + throw new Error(`No description for ${title}`); } return acc; @@ -205,33 +165,37 @@ ${el.description} }; /** - * Entry function + * Builds about wiki pages for scriptlets and redirects */ -function init() { +const buildWikiAboutPages = () => { try { - const scriptletsMarkdownData = generateMD(manageDataFromFiles().scriptletsData); - const redirectsMarkdownData = generateMD(manageDataFromFiles().redirectsData); - const staticRedirectsMarkdownData = mdForStaticRedirects(); - const blockingRedirectsMarkdownData = mdForBlockingRedirects(); + const filesData = manageDataFromFiles(); + const scriptletsMarkdownData = getMarkdownData(filesData.scriptletsData); + const redirectsMarkdownData = getMarkdownData(filesData.redirectsData); + const staticRedirectsMarkdownData = getMarkdownDataForStaticRedirects(); + const blockingRedirectsMarkdownData = getMarkdownDataForBlockingRedirects(); + + const scriptletsPageContent = `## Available Scriptlets +${scriptletsMarkdownData.list}* * * +${scriptletsMarkdownData.body}`; + fs.writeFileSync( + path.resolve(__dirname, aboutScriptletsPath), + scriptletsPageContent, + ); /* eslint-disable max-len */ - const scriptletsAbout = `## Available Scriptlets\n${scriptletsMarkdownData.list}* * *\n${scriptletsMarkdownData.body}`; - fs.writeFileSync(path.resolve(__dirname, ABOUT_SCRIPTLETS_PATH), scriptletsAbout); - - const redirectsAbout = `## Available Redirect resources + const redirectsPageContent = `## Available Redirect resources ${staticRedirectsMarkdownData.list}${redirectsMarkdownData.list}${blockingRedirectsMarkdownData.list}* * * ${staticRedirectsMarkdownData.body}${redirectsMarkdownData.body}${blockingRedirectsMarkdownData.body}`; /* eslint-enable max-len */ - fs.writeFileSync(path.resolve(__dirname, ABOUT_REDIRECTS_PATH), redirectsAbout); + fs.writeFileSync( + path.resolve(__dirname, aboutRedirectsPath), + redirectsPageContent, + ); } catch (e) { // eslint-disable-next-line no-console console.log(e.message); } -} - -init(); - -module.exports = { - redirectsFilesList, - getDataFromFiles, }; + +buildWikiAboutPages(); diff --git a/scripts/build-redirects.js b/scripts/build-redirects.js index eccc95e47..3f2293da7 100644 --- a/scripts/build-redirects.js +++ b/scripts/build-redirects.js @@ -13,19 +13,20 @@ import generateHtml from 'rollup-plugin-generate-html'; import { minify } from 'terser'; import * as redirectsList from '../src/redirects/redirects-list'; import { version } from '../package.json'; -import { redirectsFilesList, getDataFromFiles } from './build-docs'; -import { writeFile } from './helpers'; import { rollupStandard } from './rollup-runners'; +import { writeFileAsync, getDataFromFiles } from './helpers'; +import { redirectsFilenames, REDIRECTS_SRC_RELATIVE_DIR_PATH } from './constants'; const FILE_NAME = 'redirects.yml'; const CORELIBS_FILE_NAME = 'redirects.json'; const PATH_TO_DIST = './dist'; + const RESULT_PATH = path.resolve(PATH_TO_DIST, FILE_NAME); const REDIRECT_FILES_PATH = path.resolve(PATH_TO_DIST, 'redirect-files'); const CORELIBS_RESULT_PATH = path.resolve(PATH_TO_DIST, CORELIBS_FILE_NAME); +// TODO: check if constants may be used const DIST_REDIRECT_FILES = 'dist/redirect-files'; -const REDIRECTS_DIRECTORY = '../src/redirects'; const STATIC_REDIRECTS_PATH = './src/redirects/static-redirects.yml'; const BLOCKING_REDIRECTS_PATH = './src/redirects/blocking-redirects.yml'; const banner = `# @@ -134,9 +135,12 @@ const getJsRedirects = async (options = {}) => { }; })); - const redirectsDescriptions = getDataFromFiles(redirectsFilesList, REDIRECTS_DIRECTORY) - .flat(1); + const redirectsDescriptions = getDataFromFiles( + redirectsFilenames, + REDIRECTS_SRC_RELATIVE_DIR_PATH, + ).flat(1); + // TODO: seems like duplicate of already existed code /** * Returns first line of describing comment from redirect resource file * @param {string} rrName redirect resource name @@ -166,7 +170,7 @@ const getJsRedirects = async (options = {}) => { throw new Error(`Couldn't find source for non-static redirect: ${fileName}`); }; - const jsRedirects = redirectsFilesList.map((filename) => complementJsRedirects(filename)); + const jsRedirects = redirectsFilenames.map((filename) => complementJsRedirects(filename)); return jsRedirects; }; @@ -198,7 +202,7 @@ export const getPreparedRedirects = async (options) => { const buildJsRedirectFiles = async (redirectsData) => { const saveRedirectData = async (redirect) => { const redirectPath = `${REDIRECT_FILES_PATH}/${redirect.file}`; - await writeFile(redirectPath, redirect.content); + await writeFileAsync(redirectPath, redirect.content); }; await Promise.all(Object.values(redirectsData) @@ -226,9 +230,9 @@ const buildStaticRedirectFiles = async (redirectsData) => { // replace them all because base64 isn't supposed to have them contentToWrite = content.replace(/(\r\n|\n|\r|\s)/gm, ''); const buff = Buffer.from(contentToWrite, 'base64'); - await writeFile(redirectPath, buff); + await writeFileAsync(redirectPath, buff); } else { - await writeFile(redirectPath, contentToWrite); + await writeFileAsync(redirectPath, contentToWrite); } }; @@ -247,7 +251,7 @@ const buildRedirectsYamlFile = async (mergedRedirects) => { // add version and title to the top yamlRedirects = `${banner}${yamlRedirects}`; - await writeFile(RESULT_PATH, yamlRedirects); + await writeFileAsync(RESULT_PATH, yamlRedirects); }; export const prebuildRedirects = async () => { @@ -368,7 +372,7 @@ export const buildRedirectsForCorelibs = async () => { try { const jsonString = JSON.stringify(base64Redirects, null, 4); - await writeFile(CORELIBS_RESULT_PATH, jsonString); + await writeFileAsync(CORELIBS_RESULT_PATH, jsonString); } catch (e) { // eslint-disable-next-line no-console console.log(`Couldn't save to ${CORELIBS_RESULT_PATH}, because of: ${e.message}`); diff --git a/scripts/check-sources-updates.js b/scripts/check-sources-updates.js index fc4ba4a66..ba6700d20 100644 --- a/scripts/check-sources-updates.js +++ b/scripts/check-sources-updates.js @@ -191,7 +191,7 @@ async function checkForUBOScriptletsUpdates() { /** * UBO redirects github page */ -const UBO_REDIRECTS_DIRECTORY_FILE = 'https://raw.githubusercontent.com/gorhill/uBlock/master/src/js/redirect-engine.js'; +const UBO_REDIRECTS_DIRECTORY_FILE = 'https://raw.githubusercontent.com/gorhill/uBlock/master/src/js/redirect-resources.js'; /** * Make request to UBO repo(master), parses and returns the list of UBO redirects @@ -201,7 +201,7 @@ async function getCurrentUBORedirects() { let { data } = await axios.get(UBO_REDIRECTS_DIRECTORY_FILE); console.log('Done.'); - const startTrigger = 'const redirectableResources = new Map(['; + const startTrigger = 'export default new Map(['; const endTrigger = ']);'; const startIndex = data.indexOf(startTrigger); @@ -270,7 +270,7 @@ async function getCurrentABPSnippets() { // eslint-disable-line no-unused-vars /** * Checks for ABP Snippets updates */ -async function checkForABPScriptletssUpdates() { +async function checkForABPScriptletsUpdates() { const oldList = getScriptletsFromTable('abp'); // ABP_SNIPPETS_FILE is unavailable // TODO: fix later, AG-11891 @@ -333,7 +333,7 @@ async function checkForABPRedirectsUpdates() { const UBORedirectsDiff = await checkForUBORedirectsUpdates(); const UBOScriptletsDiff = await checkForUBOScriptletsUpdates(); const ABPRedirectsDiff = await checkForABPRedirectsUpdates(); - const ABPScriptletsDiff = await checkForABPScriptletssUpdates(); + const ABPScriptletsDiff = await checkForABPScriptletsUpdates(); if (UBORedirectsDiff) { markTableWithDiff(UBORedirectsDiff, 'redirects', 'ubo'); @@ -368,6 +368,6 @@ async function checkForABPRedirectsUpdates() { ${added.length ? `Added: ${added}.` : ''} `; - throw new Error(message); + console.log(message); } }()); diff --git a/scripts/constants.js b/scripts/constants.js index 26a21d482..8f9c90c00 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -1,25 +1,53 @@ const path = require('path'); +const { getFilesList } = require('./helpers'); + /** * Rules which were removed from the list should be marked with it */ const REMOVED_MARKER = '(removed)'; -const COMPATIBILITY_TABLE_INPUT_FILE = './compatibility-table.json'; -const COMPATIBILITY_TABLE_OUTPUT_FILE = '../wiki/compatibility-table.md'; - +const COMPATIBILITY_TABLE_INPUT_FILENAME = 'compatibility-table.json'; /** - * Path to compatibility data source json + * Path to **input** compatibility data source json */ -const COMPATIBILITY_TABLE_DATA_PATH = path.resolve(__dirname, COMPATIBILITY_TABLE_INPUT_FILE); +const COMPATIBILITY_TABLE_DATA_PATH = path.resolve(__dirname, COMPATIBILITY_TABLE_INPUT_FILENAME); -/** - * Path to file with compatibility tables - */ -const WIKI_COMPATIBILITY_TABLE_PATH = path.resolve(__dirname, COMPATIBILITY_TABLE_OUTPUT_FILE); +const WIKI_DIR_PATH = '../wiki'; + +const SRC_RELATIVE_DIR = '../src'; +const SRC_SCRIPTLETS_SUB_DIR = 'scriptlets'; +const SRC_REDIRECTS_SUB_DIR = 'redirects'; + +const SCRIPTLETS_SRC_RELATIVE_DIR_PATH = `${SRC_RELATIVE_DIR}/${SRC_SCRIPTLETS_SUB_DIR}`; +const REDIRECTS_SRC_RELATIVE_DIR_PATH = `${SRC_RELATIVE_DIR}/${SRC_REDIRECTS_SUB_DIR}`; + +// files which are not scriptlets in the source directory +const NON_SCRIPTLETS_FILES = [ + 'index.js', + 'scriptlets.js', + 'scriptlets-list.js', + 'scriptlets-wrapper.js', + 'scriptlets-umd-wrapper.js', +]; +const scriptletsFilenames = getFilesList(SCRIPTLETS_SRC_RELATIVE_DIR_PATH) + .filter((el) => !NON_SCRIPTLETS_FILES.includes(el)); + +// files which are not redirects in the source directory +const NON_REDIRECTS_FILES = [ + 'index.js', + 'redirects.js', + 'redirects-list.js', +]; +const redirectsFilenames = getFilesList(REDIRECTS_SRC_RELATIVE_DIR_PATH) + .filter((el) => !NON_REDIRECTS_FILES.includes(el)); module.exports = { REMOVED_MARKER, COMPATIBILITY_TABLE_DATA_PATH, - WIKI_COMPATIBILITY_TABLE_PATH, + WIKI_DIR_PATH, + SCRIPTLETS_SRC_RELATIVE_DIR_PATH, + REDIRECTS_SRC_RELATIVE_DIR_PATH, + scriptletsFilenames, + redirectsFilenames, }; diff --git a/scripts/helpers.js b/scripts/helpers.js index 4863bbc8e..b3d126b64 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1,9 +1,125 @@ -import path from 'path'; -import fs from 'fs-extra'; +const path = require('path'); +const fs = require('fs-extra'); +const dox = require('dox'); -export const writeFile = async (filePath, content) => { +/** + * Asynchronously writes data to a file, replacing the file if it already exists. + * + * @param {string} filePath absolute path to file + * @param {string} content content to write to the file + */ +const writeFile = async (filePath, content) => { const dirname = path.dirname(filePath); await fs.ensureDir(dirname); await fs.writeFile(filePath, content); }; + +/** + * Gets list of `.js` files in directory + * @param {string} relativeDirPath relative path to directory + * @returns {string[]} array of file names + */ +const getFilesList = (relativeDirPath) => { + return fs.readdirSync(path.resolve(__dirname, relativeDirPath), { encoding: 'utf8' }) + .filter((el) => el.includes('.js')); +}; + +/** + * @typedef {Object} CommentTag + * @property {string} type tag name + * @property {string} string text following the tag + */ + +/** + * Returns parsed tags data which we use to describe the sources: + * - `@scriptlet`/`@redirect` to describe the type and name of source; + * - `@description` actual description for scriptlet or redirect. + * required comments from file. + * In one file might be comments describing scriptlet and redirect as well. + * + * @param {string} filePath absolute path to file + * + * @returns {CommentTag[]} + */ +const getDescribingCommentTags = (filePath) => { + const fileContent = fs.readFileSync(filePath, { encoding: 'utf8' }); + const parsedFileComments = dox.parseComments(fileContent); + const describingComment = parsedFileComments + // get rid of not needed comments data + .filter(({ tags }) => { + // '@scriptlet', '@redirect', and 'description' + // are parser by dox.parseComments() as `tags` + if (tags.length === 0) { + return false; + } + const [base] = tags; + return base?.type === 'scriptlet' + || base?.type === 'redirect'; + }); + + if (describingComment.length === 0) { + throw new Error(`No description in ${filePath}. +Please add one OR edit the list of NON_SCRIPTLETS_FILES / NON_REDIRECTS_FILES.`); + } + + if (describingComment.length > 1) { + throw new Error(`File should have one description comment: ${filePath}.`); + } + + // eventually only one comment data item should left + return describingComment[0].tags; +}; + +/** + * @typedef {Object} DescribingCommentData + * + * Collected data from jsdoc-type comment for every scriptlet or redirect. + * + * @property {string} type parsed instance tag: + * 'scriptlet' for '@scriptlet', 'redirect' for '@redirect' + * @property {string} name name of instance which goes after the instance tag + * @property {string} description description, goes after `@description` tag + * @property {string} source relative path to source of scriptlet or redirect from wiki/about page + */ + +/** + * Converts parsed comment to data object. + * + * @param {CommentTag[]} commentTags parsed tags from describing comment + * @param {string} source relative path to file + * + * @returns {DescribingCommentData} + */ +const prepareCommentsData = (commentTags, source) => { + const [base, sup] = commentTags; + return { + type: base.type, + name: base.string, + description: sup.string, + source, + }; +}; + +/** + * Gets data objects which describe every required comment in one directory + * @param {string[]} filesList list of files in directory + * @param {string} relativeDirPath relative path to directory + * + * @returns {DescribingCommentData} + */ +const getDataFromFiles = (filesList, relativeDirPath) => { + const pathToDir = path.resolve(__dirname, relativeDirPath); + return filesList.map((file) => { + const pathToFile = path.resolve(pathToDir, file); + const requiredCommentTags = getDescribingCommentTags(pathToFile); + + return prepareCommentsData(requiredCommentTags, `${relativeDirPath}/${file}`); + }); +}; + +module.exports = { + writeFile, + getFilesList, + getDataFromFiles, +}; diff --git a/src/helpers/cookie-utils.js b/src/helpers/cookie-utils.js index 13766524e..a7532cabb 100644 --- a/src/helpers/cookie-utils.js +++ b/src/helpers/cookie-utils.js @@ -1,47 +1,91 @@ import { nativeIsNaN } from './number-utils'; /** - * Prepares cookie string if given parameters are ok - * @param {string} name cookie name to set - * @param {string} value cookie value to set - * @param {string} path cookie path to set, 'none' for no path - * @returns {string|null} cookie string if ok OR null if not + * Checks whether the input path is supported + * + * @param {string} rawPath input path + * + * @returns {boolean} */ -export const prepareCookie = (name, value, path) => { - if (!name || !value) { - return null; +export const isValidCookieRawPath = (rawPath) => rawPath === '/' || rawPath === 'none'; + +/** + * Returns 'path=/' if rawPath is '/' + * or empty string '' for other cases, `rawPath === 'none'` included + * + * @param {string} rawPath + * + * @returns {string} + */ +export const getCookiePath = (rawPath) => { + if (rawPath === '/') { + return 'path=/'; } + // otherwise do not set path as invalid + // the same for pathArg === 'none' + // + return ''; +}; +/** + * Combines input cookie name, value, and path into string. + * + * @param {string} rawName + * @param {string} rawValue + * @param {string} rawPath + * + * @returns {string} string OR `null` if path is not supported + */ +export const concatCookieNameValuePath = (rawName, rawValue, rawPath) => { const log = console.log.bind(console); // eslint-disable-line no-console + if (!isValidCookieRawPath(rawPath)) { + log(`Invalid cookie path: '${rawPath}'`); + return null; + } + // eslint-disable-next-line max-len + return `${encodeURIComponent(rawName)}=${encodeURIComponent(rawValue)}; ${getCookiePath(rawPath)}`; +}; - let valueToSet; +/** + * Gets supported cookie value + * + * @param {string} value input cookie value + * + * @returns {string|null} valid cookie string if ok OR null if not + */ +export const getLimitedCookieValue = (value) => { + if (!value) { + return null; + } + const log = console.log.bind(console); // eslint-disable-line no-console + let validValue; if (value === 'true') { - valueToSet = 'true'; + validValue = 'true'; } else if (value === 'True') { - valueToSet = 'True'; + validValue = 'True'; } else if (value === 'false') { - valueToSet = 'false'; + validValue = 'false'; } else if (value === 'False') { - valueToSet = 'False'; + validValue = 'False'; } else if (value === 'yes') { - valueToSet = 'yes'; + validValue = 'yes'; } else if (value === 'Yes') { - valueToSet = 'Yes'; + validValue = 'Yes'; } else if (value === 'Y') { - valueToSet = 'Y'; + validValue = 'Y'; } else if (value === 'no') { - valueToSet = 'no'; + validValue = 'no'; } else if (value === 'ok') { - valueToSet = 'ok'; + validValue = 'ok'; } else if (value === 'OK') { - valueToSet = 'OK'; + validValue = 'OK'; } else if (/^\d+$/.test(value)) { - valueToSet = parseFloat(value); - if (nativeIsNaN(valueToSet)) { + validValue = parseFloat(value); + if (nativeIsNaN(validValue)) { log(`Invalid cookie value: '${value}'`); return null; } - if (Math.abs(valueToSet) < 0 || Math.abs(valueToSet) > 15) { + if (Math.abs(validValue) < 0 || Math.abs(validValue) > 15) { log(`Invalid cookie value: '${value}'`); return null; } @@ -49,20 +93,7 @@ export const prepareCookie = (name, value, path) => { return null; } - let pathToSet; - if (path === '/') { - pathToSet = 'path=/'; - } else if (path === 'none') { - pathToSet = ''; - } else { - log(`Invalid cookie path: '${path}'`); - return null; - } - - // eslint-disable-next-line max-len - const cookieData = `${encodeURIComponent(name)}=${encodeURIComponent(valueToSet)}; ${pathToSet}`; - - return cookieData; + return validValue; }; /** @@ -94,3 +125,24 @@ export const parseCookieString = (cookieString) => { return cookieData; }; + +/** + * Check if cookie with specified name and value is present in a cookie string + * @param {string} cookieString + * @param {string} name + * @param {string} value + * @returns {boolean} + */ +export const isCookieSetWithValue = (cookieString, name, value) => { + return cookieString.split(';') + .some((cookieStr) => { + const pos = cookieStr.indexOf('='); + if (pos === -1) { + return false; + } + const cookieName = cookieStr.slice(0, pos).trim(); + const cookieValue = cookieStr.slice(pos + 1).trim(); + + return name === cookieName && value === cookieValue; + }); +}; diff --git a/src/scriptlets/scriptlets-list.js b/src/scriptlets/scriptlets-list.js index 9a14ecf04..0a9141f4a 100644 --- a/src/scriptlets/scriptlets-list.js +++ b/src/scriptlets/scriptlets-list.js @@ -50,5 +50,6 @@ export * from './prevent-element-src-loading'; export * from './no-topics'; export * from './trusted-replace-xhr-response'; export * from './xml-prune'; +export * from './trusted-set-cookie'; export * from './trusted-replace-fetch-response'; export * from './trusted-set-local-storage-item'; diff --git a/src/scriptlets/set-cookie-reload.js b/src/scriptlets/set-cookie-reload.js index 037c589d7..128126fb3 100644 --- a/src/scriptlets/set-cookie-reload.js +++ b/src/scriptlets/set-cookie-reload.js @@ -1,7 +1,13 @@ import { hit, nativeIsNaN, - prepareCookie, + isCookieSetWithValue, + getLimitedCookieValue, + concatCookieNameValuePath, + // following helpers should be imported and injected + // because they are used by helpers above + isValidCookieRawPath, + getCookiePath, } from '../helpers/index'; /** @@ -40,25 +46,20 @@ import { * ``` */ export function setCookieReload(source, name, value, path = '/') { - const isCookieSetWithValue = (name, value) => { - return document.cookie.split(';') - .some((cookieStr) => { - const pos = cookieStr.indexOf('='); - if (pos === -1) { - return false; - } - const cookieName = cookieStr.slice(0, pos).trim(); - const cookieValue = cookieStr.slice(pos + 1).trim(); + if (isCookieSetWithValue(name, value)) { + return; + } - return name === cookieName && value === cookieValue; - }); - }; + // eslint-disable-next-line no-console + const log = console.log.bind(console); - if (isCookieSetWithValue(name, value)) { + const validValue = getLimitedCookieValue(value); + if (validValue === null) { + log(`Invalid cookie value: '${validValue}'`); return; } - const cookieData = prepareCookie(name, value, path); + const cookieData = concatCookieNameValuePath(name, validValue, path); if (cookieData) { document.cookie = cookieData; @@ -66,7 +67,7 @@ export function setCookieReload(source, name, value, path = '/') { // Only reload the page if cookie was set // https://github.com/AdguardTeam/Scriptlets/issues/212 - if (isCookieSetWithValue(name, value)) { + if (isCookieSetWithValue(document.cookie, name, value)) { window.location.reload(); } } @@ -76,4 +77,12 @@ setCookieReload.names = [ 'set-cookie-reload', ]; -setCookieReload.injections = [hit, nativeIsNaN, prepareCookie]; +setCookieReload.injections = [ + hit, + nativeIsNaN, + isCookieSetWithValue, + getLimitedCookieValue, + concatCookieNameValuePath, + isValidCookieRawPath, + getCookiePath, +]; diff --git a/src/scriptlets/set-cookie.js b/src/scriptlets/set-cookie.js index 441b368eb..c686ef498 100644 --- a/src/scriptlets/set-cookie.js +++ b/src/scriptlets/set-cookie.js @@ -1,4 +1,14 @@ -import { hit, nativeIsNaN, prepareCookie } from '../helpers/index'; +import { + hit, + nativeIsNaN, + isCookieSetWithValue, + getLimitedCookieValue, + concatCookieNameValuePath, + // following helpers should be imported and injected + // because they are used by helpers above + isValidCookieRawPath, + getCookiePath, +} from '../helpers/index'; /* eslint-disable max-len */ /** @@ -36,7 +46,16 @@ import { hit, nativeIsNaN, prepareCookie } from '../helpers/index'; */ /* eslint-enable max-len */ export function setCookie(source, name, value, path = '/') { - const cookieData = prepareCookie(name, value, path); + // eslint-disable-next-line no-console + const log = console.log.bind(console); + + const validValue = getLimitedCookieValue(value); + if (validValue === null) { + log(`Invalid cookie value: '${validValue}'`); + return; + } + + const cookieData = concatCookieNameValuePath(name, validValue, path); if (cookieData) { hit(source); @@ -48,4 +67,12 @@ setCookie.names = [ 'set-cookie', ]; -setCookie.injections = [hit, nativeIsNaN, prepareCookie]; +setCookie.injections = [ + hit, + nativeIsNaN, + isCookieSetWithValue, + getLimitedCookieValue, + concatCookieNameValuePath, + isValidCookieRawPath, + getCookiePath, +]; diff --git a/src/scriptlets/trusted-replace-fetch-response.js b/src/scriptlets/trusted-replace-fetch-response.js index a3472c501..7dc999643 100644 --- a/src/scriptlets/trusted-replace-fetch-response.js +++ b/src/scriptlets/trusted-replace-fetch-response.js @@ -172,8 +172,7 @@ export function trustedReplaceFetchResponse(source, pattern = '', replacement = .catch(() => { // log if response body can't be converted to a string const fetchDataStr = objectToString(fetchData); - const logMessage = `log: Response body can't be converted to text: ${fetchDataStr}`; - log(source, logMessage); + log(`Response body can't be converted to text: ${fetchDataStr}`); return Reflect.apply(target, thisArg, args); }); }) diff --git a/src/scriptlets/trusted-set-cookie.js b/src/scriptlets/trusted-set-cookie.js new file mode 100644 index 000000000..9e2f05a80 --- /dev/null +++ b/src/scriptlets/trusted-set-cookie.js @@ -0,0 +1,165 @@ +import { + hit, + nativeIsNaN, + isCookieSetWithValue, + concatCookieNameValuePath, + // following helpers should be imported and injected + // because they are used by helpers above + isValidCookieRawPath, + getCookiePath, +} from '../helpers/index'; + +/* eslint-disable max-len */ +/** + * @scriptlet trusted-set-cookie + * + * @description + * Sets a cookie with arbitrary name and value, with optional path + * and the ability to reload the page after cookie was set. + * + * **Syntax** + * ``` + * example.org#%#//scriptlet('trusted-set-cookie', name, value[, offsetExpiresSec[, reload[, path]]]) + * ``` + * + * - `name` - required, cookie name to be set + * - `value` - required, cookie value. Possible values: + * - arbitrary value + * - empty string for no value + * - `$now$` keyword for setting current time + * - 'offsetExpiresSec' - optional, offset from current time in seconds, after which cookie should expire; defaults to no offset + * Possible values: + * - positive integer in seconds + * - `1year` keyword for setting expiration date to one year + * - `1day` keyword for setting expiration date to one day + * - 'reload' - optional, boolean. Argument for reloading page after cookie is set. Defaults to `false` + * - `path` - optional, argument for setting cookie path, defaults to `/`; possible values: + * - `/` — root path + * - `none` — to set no path at all + * + * **Examples** + * 1. Set cookie + * ``` + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'accept') + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', '1-accept_1') + * ``` + * + * 2. Set cookie with `new Date().getTime()` value + * ``` + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', '$now') + * ``` + * + * 3. Set cookie which will expire in 3 days + * ``` + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'accept', '259200') + * ``` + * + * 4. Set cookie which will expire in one year + * ``` + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'accept', '1year') + * ``` + * 5. Reload the page if cookie was successfully set + * ``` + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'decline', '', 'true') + * ``` + * + * 6. Set cookie with no path + * ``` + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'decline', '', '', 'none') + * ``` + */ +/* eslint-enable max-len */ + +export function trustedSetCookie(source, name, value, offsetExpiresSec = '', reload = 'false', path = '/') { + // eslint-disable-next-line no-console + const log = console.log.bind(console); + + if (typeof name === 'undefined') { + log('Cookie name should be specified.'); + return; + } + + if (typeof value === 'undefined') { + log('Cookie value should be specified.'); + return; + } + + // Prevent infinite reloads if cookie was already set or blocked by the browser + // https://github.com/AdguardTeam/Scriptlets/issues/212 + if (reload === 'true' && isCookieSetWithValue(document.cookie, name, value)) { + return; + } + + const NOW_VALUE_KEYWORD = '$now$'; + const ONE_YEAR_EXPIRATION_KEYWORD = '1year'; + const ONE_DAY_EXPIRATION_KEYWORD = '1day'; + + let parsedValue; + + if (value === NOW_VALUE_KEYWORD) { + // Set cookie value to current time if corresponding keyword was passed + const date = new Date(); + const currentTime = date.getTime(); + + parsedValue = currentTime.toString(); + } else { + parsedValue = value; + } + + let cookieToSet = concatCookieNameValuePath(name, parsedValue, path); + if (!cookieToSet) { + return; + } + + // Set expiration date if offsetExpiresSec was passed + if (offsetExpiresSec) { + const MS_IN_SEC = 1000; + const SECONDS_IN_YEAR = 365 * 24 * 60 * 60; + const SECONDS_IN_DAY = 24 * 60 * 60; + + let parsedOffsetExpiresSec; + + // Set predefined expire value if corresponding keyword was passed + if (offsetExpiresSec === ONE_YEAR_EXPIRATION_KEYWORD) { + parsedOffsetExpiresSec = SECONDS_IN_YEAR; + } else if (offsetExpiresSec === ONE_DAY_EXPIRATION_KEYWORD) { + parsedOffsetExpiresSec = SECONDS_IN_DAY; + } else { + parsedOffsetExpiresSec = Number.parseInt(offsetExpiresSec, 10); + + // If offsetExpiresSec has been parsed to NaN - do not set cookie at all + if (Number.isNaN(parsedOffsetExpiresSec)) { + log(`log: Invalid offsetExpiresSec value: ${offsetExpiresSec}`); + return; + } + } + + const expires = Date.now() + parsedOffsetExpiresSec * MS_IN_SEC; + cookieToSet += ` expires=${new Date(expires).toUTCString()};`; + } + + if (cookieToSet) { + document.cookie = cookieToSet; + hit(source); + + // Only reload the page if cookie was set + // https://github.com/AdguardTeam/Scriptlets/issues/212 + if (reload === 'true' && isCookieSetWithValue(document.cookie, name, value)) { + window.location.reload(); + } + } +} + +trustedSetCookie.names = [ + 'trusted-set-cookie', + // trusted scriptlets support no aliases +]; + +trustedSetCookie.injections = [ + hit, + nativeIsNaN, + isCookieSetWithValue, + concatCookieNameValuePath, + isValidCookieRawPath, + getCookiePath, +]; diff --git a/tests/helpers.js b/tests/helpers.js index 8ed45f9f0..bfda76242 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -59,4 +59,12 @@ export const runRedirect = (name, verbose = true) => { evalWrapper(resultString); }; +/** + * Clear cookie by name + * @param {string} cName + */ +export const clearCookie = (cName) => { + document.cookie = `${cName}=; max-age=0`; +}; + export const isSafariBrowser = () => navigator.vendor === 'Apple Computer, Inc.'; diff --git a/tests/scriptlets/index.test.js b/tests/scriptlets/index.test.js index 90e785e1a..06adf3bbf 100644 --- a/tests/scriptlets/index.test.js +++ b/tests/scriptlets/index.test.js @@ -47,5 +47,6 @@ import './no-topics.test'; import './trusted-replace-xhr-response.test'; import './xml-prune.test'; import './trusted-click-element.test'; +import './trusted-set-cookie.test'; import './trusted-replace-fetch-response.test'; import './trusted-set-local-storage-item.test'; diff --git a/tests/scriptlets/set-cookie-reload.test.js b/tests/scriptlets/set-cookie-reload.test.js index 0f6dd04a8..6075424d3 100644 --- a/tests/scriptlets/set-cookie-reload.test.js +++ b/tests/scriptlets/set-cookie-reload.test.js @@ -1,5 +1,9 @@ /* eslint-disable no-underscore-dangle */ -import { runScriptlet, clearGlobalProps } from '../helpers'; +import { + runScriptlet, + clearGlobalProps, + clearCookie, +} from '../helpers'; const { test, module } = QUnit; const name = 'set-cookie-reload'; @@ -14,10 +18,6 @@ const afterEach = () => { clearGlobalProps('hit', '__debug'); }; -const clearCookie = (cName) => { - document.cookie = `${cName}=; max-age=0`; -}; - module(name, { beforeEach, afterEach }); test('Set cookie with valid value', (assert) => { diff --git a/tests/scriptlets/set-cookie.test.js b/tests/scriptlets/set-cookie.test.js index 869e70cf9..31c4338bd 100644 --- a/tests/scriptlets/set-cookie.test.js +++ b/tests/scriptlets/set-cookie.test.js @@ -1,5 +1,9 @@ /* eslint-disable no-underscore-dangle */ -import { runScriptlet, clearGlobalProps } from '../helpers'; +import { + runScriptlet, + clearGlobalProps, + clearCookie, +} from '../helpers'; const { test, module } = QUnit; const name = 'set-cookie'; @@ -16,10 +20,6 @@ const afterEach = () => { module(name, { beforeEach, afterEach }); -const clearCookie = (cName) => { - document.cookie = `${cName}=; max-age=0`; -}; - test('Set cookie with valid value', (assert) => { let cName = '__test-cookie_OK'; let cValue = 'OK'; diff --git a/tests/scriptlets/trusted-click-element.test.js b/tests/scriptlets/trusted-click-element.test.js index bde73da64..663f60625 100644 --- a/tests/scriptlets/trusted-click-element.test.js +++ b/tests/scriptlets/trusted-click-element.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle, no-console */ import { runScriptlet, clearGlobalProps } from '../helpers'; -import { prepareCookie } from '../../src/helpers'; +import { concatCookieNameValuePath } from '../../src/helpers'; const { test, module } = QUnit; const name = 'trusted-click-element'; @@ -162,7 +162,7 @@ test('Multiple elements clicked, non-ordered render', (assert) => { test('extraMatch - single cookie match, matched', (assert) => { const cookieKey1 = 'first'; - const cookieData = prepareCookie(cookieKey1, 'true'); + const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/'); document.cookie = cookieData; const EXTRA_MATCH_STR = `cookie:${cookieKey1}`; @@ -190,7 +190,7 @@ test('extraMatch - single cookie match, matched', (assert) => { test('extraMatch - single cookie match, not matched', (assert) => { const cookieKey1 = 'first'; const cookieKey2 = 'second'; - const cookieData = prepareCookie(cookieKey1, 'true'); + const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/'); document.cookie = cookieData; const EXTRA_MATCH_STR = `cookie:${cookieKey2}`; @@ -218,7 +218,7 @@ test('extraMatch - single cookie match, not matched', (assert) => { test('extraMatch - string+regex cookie input, matched', (assert) => { const cookieKey1 = 'first'; const cookieVal1 = 'true'; - const cookieData1 = prepareCookie(cookieKey1, cookieVal1); + const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/'); document.cookie = cookieData1; const EXTRA_MATCH_STR = 'cookie:/firs/=true'; @@ -299,13 +299,13 @@ test('extraMatch - single localStorage match, not matched', (assert) => { test('extraMatch - complex string+regex cookie input & whitespaces & comma in regex, matched', (assert) => { const cookieKey1 = 'first'; const cookieVal1 = 'true'; - const cookieData1 = prepareCookie(cookieKey1, cookieVal1); + const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/'); const cookieKey2 = 'sec'; const cookieVal2 = '1-1'; - const cookieData2 = prepareCookie(cookieKey2, cookieVal2); + const cookieData2 = concatCookieNameValuePath(cookieKey2, cookieVal2, '/'); const cookieKey3 = 'third'; const cookieVal3 = 'true'; - const cookieData3 = prepareCookie(cookieKey3, cookieVal3); + const cookieData3 = concatCookieNameValuePath(cookieKey3, cookieVal3, '/'); document.cookie = cookieData1; document.cookie = cookieData2; diff --git a/tests/scriptlets/trusted-set-cookie.test.js b/tests/scriptlets/trusted-set-cookie.test.js new file mode 100644 index 000000000..3fecbf393 --- /dev/null +++ b/tests/scriptlets/trusted-set-cookie.test.js @@ -0,0 +1,116 @@ +/* eslint-disable no-underscore-dangle */ +import { + runScriptlet, + clearGlobalProps, + clearCookie, +} from '../helpers'; + +const { test, module } = QUnit; +const name = 'trusted-set-cookie'; + +const beforeEach = () => { + window.__debug = () => { + window.hit = 'FIRED'; + }; +}; + +const afterEach = () => { + clearGlobalProps('hit', '__debug'); +}; + +module(name, { beforeEach, afterEach }); + +test('Set cookie string', (assert) => { + let cName = '__test-cookie_OK'; + let cValue = 'OK'; + runScriptlet(name, [cName, cValue]); + assert.strictEqual(window.hit, 'FIRED', 'Hit was fired'); + assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set'); + assert.strictEqual(document.cookie.includes(cValue), true, 'Cookie value has been set'); + clearCookie(cName); + + cName = '__test-cookie_0'; + cValue = 0; + runScriptlet(name, [cName, cValue, '', '']); + assert.strictEqual(window.hit, 'FIRED', 'Hit was fired'); + assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set'); + assert.strictEqual(document.cookie.includes(cValue), true, 'Cookie value has been set'); + clearCookie(cName); + + cName = 'trackingSettings'; + cValue = '{%22ads%22:false%2C%22performance%22:false}'; + runScriptlet(name, [cName, cValue]); + + assert.strictEqual(window.hit, 'FIRED', 'Hit was fired'); + assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set'); + assert.strictEqual(document.cookie.includes(encodeURIComponent(cValue)), true, 'Cookie value has been set'); + clearCookie(cName); +}); + +test('Set cookie with current time value', (assert) => { + const cName = '__test-cookie_OK'; + const cValue = '$now$'; + + runScriptlet(name, [cName, cValue]); + + assert.strictEqual(window.hit, 'FIRED', 'Hit was fired'); + assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set'); + + // Some time will pass between calling scriptlet + // and qunit running assertion + const tolerance = 20; + const cookieValue = document.cookie.split('=')[1]; + const currentTime = new Date().getTime(); + const timeDiff = currentTime - cookieValue; + + assert.ok(timeDiff < tolerance, 'Cookie value has been set to current time'); + + clearCookie(cName); +}); + +test('Set cookie with expires', (assert) => { + const cName = '__test-cookie_expires'; + const cValue = 'expires'; + const expiresSec = 2; + + runScriptlet(name, [cName, cValue, `${expiresSec}`]); + + assert.strictEqual(window.hit, 'FIRED', 'Hit was fired'); + assert.strictEqual(document.cookie.includes(cName), true, 'Cookie name has been set'); + assert.strictEqual(document.cookie.includes(cValue), true, 'Cookie value has been set'); + + const done = assert.async(); + + setTimeout(() => { + assert.strictEqual(document.cookie.includes(cName), false, 'Cookie name has been deleted'); + assert.strictEqual(document.cookie.includes(cValue), false, 'Cookie value has been deleted'); + clearCookie(cName); + done(); + }, expiresSec * 1000); +}); + +test('Set cookie with negative expires', (assert) => { + const cName = '__test-cookie_expires_negative'; + const cValue = 'expires'; + const expiresSec = -2; + + runScriptlet(name, [cName, cValue, `${expiresSec}`]); + + assert.strictEqual(window.hit, 'FIRED', 'Hit was fired'); + assert.strictEqual(document.cookie.includes(cName), false, 'Cookie name has not been set'); + assert.strictEqual(document.cookie.includes(cValue), false, 'Cookie value has not been set'); + clearCookie(cName); +}); + +test('Set cookie with invalid expires', (assert) => { + const cName = '__test-cookie_expires_invalid'; + const cValue = 'expires'; + const expiresSec = 'invalid_value'; + + runScriptlet(name, [cName, cValue, `${expiresSec}`]); + + assert.strictEqual(window.hit, undefined, 'Hit was not fired'); + assert.strictEqual(document.cookie.includes(cName), false, 'Cookie name has not been set'); + assert.strictEqual(document.cookie.includes(cValue), false, 'Cookie value has not been set'); + clearCookie(cName); +});