diff --git a/.github/workflows/asana.yml b/.github/workflows/asana.yml new file mode 100644 index 000000000..0ab00debc --- /dev/null +++ b/.github/workflows/asana.yml @@ -0,0 +1,23 @@ +name: 'asana sync' +on: + pull_request_review: + pull_request_target: + types: + - opened + - edited + - closed + - reopened + - synchronize + - review_requested + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: sammacbeth/action-asana-sync@v6 + with: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + ASANA_WORKSPACE_ID: ${{ secrets.ASANA_WORKSPACE_ID }} + ASANA_PROJECT_ID: '1208598406046969' + USER_MAP: ${{ vars.USER_MAP }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 339337277..eed4eef6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,11 @@ jobs: - name: Fetch files and ensure branches exist run: | git fetch origin + if [ -f .git/shallow ]; then + echo "Shallow repo clone, unshallowing" + git fetch --unshallow + fi + git fetch --tags # Check if the 'main' branch exists, if not, create it if git rev-parse --verify main >/dev/null 2>&1; then git checkout main @@ -42,6 +47,7 @@ jobs: run: | ls -la ${{ github.workspace }}/CHANGELOG.txt cat ${{ github.workspace }}/CHANGELOG.txt + echo "Current tag is: $(git rev-list --tags --max-count=1)" - name: Checkout code from main into release branch run: | diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 98ac340e1..b5659caf7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1 +1,5 @@ -- Move release debugging (#1155) \ No newline at end of file +- Create asana.yml (#1159) +- Support conditional actions with expectations (#1144) +- Support a city/state composite value (#1158) +- ntp: customizer button (#1152) +- Add more debugging for tag release (#1156) \ No newline at end of file diff --git a/Sources/ContentScopeScripts/dist/contentScopeIsolated.js b/Sources/ContentScopeScripts/dist/contentScopeIsolated.js index dc1ae7f2c..45d895de0 100644 --- a/Sources/ContentScopeScripts/dist/contentScopeIsolated.js +++ b/Sources/ContentScopeScripts/dist/contentScopeIsolated.js @@ -7609,7 +7609,7 @@ /** * @typedef {SuccessResponse | ErrorResponse} ActionResponse * @typedef {{ result: true } | { result: false; error: string }} BooleanResult - * @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string}} Expectation + * @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string; failSilently?: boolean}} Expectation */ /** @@ -11359,6 +11359,14 @@ results.push(setValueForInput(inputElem, generateRandomInt(parseInt(element.min), parseInt(element.max)).toString())); } else if (element.type === '$generated_street_address$') { results.push(setValueForInput(inputElem, generateStreetAddress())); + + // This is a composite of existing (but separate) city and state fields + } else if (element.type === 'cityState') { + if (!Object.prototype.hasOwnProperty.call(data, 'city') || !Object.prototype.hasOwnProperty.call(data, 'state')) { + results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the keys 'city' and 'state'` }); + continue + } + results.push(setValueForInput(inputElem, data.city + ', ' + data.state)); } else { if (!Object.prototype.hasOwnProperty.call(data, element.type)) { results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the key '${element.type}'` }); @@ -11719,21 +11727,45 @@ /** * @param {Record} action - * @param {Document | HTMLElement} root - * @return {import('../types.js').ActionResponse} + * @param {Record} userData + * @param {Document} root + * @return {Promise} */ - function expectation (action, root = document) { + async function expectation (action, userData, root = document) { const results = expectMany(action.expectations, root); - const errors = results.filter(x => x.result === false).map(x => { - if ('error' in x) return x.error - return 'unknown error' - }); + // filter out good results + silent failures, leaving only fatal errors + const errors = results + .filter((x, index) => { + if (x.result === true) return false + if (action.expectations[index].failSilently) return false + return true + }).map((x) => { + return 'error' in x ? x.error : 'unknown error' + }); if (errors.length > 0) { return new ErrorResponse({ actionID: action.id, message: errors.join(', ') }) } + // only run later actions if every expectation was met + const runActions = results.every(x => x.result === true); + const secondaryErrors = []; + + if (action.actions?.length && runActions) { + for (const subAction of action.actions) { + const result = await execute(subAction, userData, root); + + if ('error' in result) { + secondaryErrors.push(result.error); + } + } + + if (secondaryErrors.length > 0) { + return new ErrorResponse({ actionID: action.id, message: secondaryErrors.join(', ') }) + } + } + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }) } @@ -11875,7 +11907,7 @@ case 'click': return click(action, data(action, inputData, 'userProfile'), root) case 'expectation': - return expectation(action, root) + return await expectation(action, data(action, inputData, 'userProfile'), root) case 'fillForm': return fillForm(action, data(action, inputData, 'extractedProfile'), root) case 'getCaptchaInfo': diff --git a/build/integration/contentScope.js b/build/integration/contentScope.js index 7865ce9fc..7df02ff5c 100644 --- a/build/integration/contentScope.js +++ b/build/integration/contentScope.js @@ -16256,7 +16256,7 @@ /** * @typedef {SuccessResponse | ErrorResponse} ActionResponse * @typedef {{ result: true } | { result: false; error: string }} BooleanResult - * @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string}} Expectation + * @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string; failSilently?: boolean}} Expectation */ /** @@ -20002,6 +20002,14 @@ results.push(setValueForInput(inputElem, generateRandomInt(parseInt(element.min), parseInt(element.max)).toString())); } else if (element.type === '$generated_street_address$') { results.push(setValueForInput(inputElem, generateStreetAddress())); + + // This is a composite of existing (but separate) city and state fields + } else if (element.type === 'cityState') { + if (!Object.prototype.hasOwnProperty.call(data, 'city') || !Object.prototype.hasOwnProperty.call(data, 'state')) { + results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the keys 'city' and 'state'` }); + continue + } + results.push(setValueForInput(inputElem, data.city + ', ' + data.state)); } else { if (!Object.prototype.hasOwnProperty.call(data, element.type)) { results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the key '${element.type}'` }); @@ -20362,21 +20370,45 @@ /** * @param {Record} action - * @param {Document | HTMLElement} root - * @return {import('../types.js').ActionResponse} + * @param {Record} userData + * @param {Document} root + * @return {Promise} */ - function expectation (action, root = document) { + async function expectation (action, userData, root = document) { const results = expectMany(action.expectations, root); - const errors = results.filter(x => x.result === false).map(x => { - if ('error' in x) return x.error - return 'unknown error' - }); + // filter out good results + silent failures, leaving only fatal errors + const errors = results + .filter((x, index) => { + if (x.result === true) return false + if (action.expectations[index].failSilently) return false + return true + }).map((x) => { + return 'error' in x ? x.error : 'unknown error' + }); if (errors.length > 0) { return new ErrorResponse({ actionID: action.id, message: errors.join(', ') }) } + // only run later actions if every expectation was met + const runActions = results.every(x => x.result === true); + const secondaryErrors = []; + + if (action.actions?.length && runActions) { + for (const subAction of action.actions) { + const result = await execute(subAction, userData, root); + + if ('error' in result) { + secondaryErrors.push(result.error); + } + } + + if (secondaryErrors.length > 0) { + return new ErrorResponse({ actionID: action.id, message: secondaryErrors.join(', ') }) + } + } + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }) } @@ -20518,7 +20550,7 @@ case 'click': return click(action, data(action, inputData, 'userProfile'), root) case 'expectation': - return expectation(action, root) + return await expectation(action, data(action, inputData, 'userProfile'), root) case 'fillForm': return fillForm(action, data(action, inputData, 'extractedProfile'), root) case 'getCaptchaInfo': diff --git a/build/integration/pages/new-tab/js/index.css b/build/integration/pages/new-tab/js/index.css index 6094cae35..b2255a201 100644 --- a/build/integration/pages/new-tab/js/index.css +++ b/build/integration/pages/new-tab/js/index.css @@ -193,6 +193,17 @@ li { margin: 0; padding: 0; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} /* pages/new-tab/app/styles/ntp-theme.css */ :root { @@ -212,7 +223,7 @@ li { --ntp-focus-outline-color: black; @media (prefers-color-scheme: dark) { --ntp-background-color: var(--color-gray-85); - --ntp-surface-background-color: var(--color-black-at-18); + --ntp-surface-background-color: #2a2a2a; --ntp-surface-border-color: var(--color-black-at-6); --ntp-text-normal: white; --ntp-focus-outline-color: white; @@ -482,6 +493,132 @@ body { fill-opacity: 0.5; } } +.Icons_customize { +} + +/* pages/new-tab/app/customizer/Customizer.module.css */ +.Customizer_root { + position: relative; +} +.Customizer_lowerRightFixed { + position: fixed; + bottom: 16px; + right: 16px; +} +.Customizer_dropdownMenu { + display: none; + position: absolute; + right: 0; + bottom: calc(100% + 10px); +} +.Customizer_show { + display: block; +} +.Customizer_customizeButton { + background-color: transparent; + border: 1px solid var(--color-black-at-9); + border-radius: 6px; + padding: 8px 12px; + cursor: pointer; + display: flex; + gap: 6px; + color: var(--color-black-at-84); + @media screen and (prefers-color-scheme: dark) { + color: var(--color-white-at-80); + border-color: var(--color-white-at-9); + } + &:focus-visible { + outline: 1px dotted var(--ntp-focus-outline-color); + } + &:hover { + background-color: var(--color-black-at-6); + border-color: var(--color-black-at-18); + @media screen and (prefers-color-scheme: dark) { + background-color: var(--color-white-at-18); + border-color: var(--color-white-at-36); + } + } + &:active { + background-color: var(--color-white-at-12); + @media screen and (prefers-color-scheme: dark) { + background-color: var(--color-white-at-24); + border-color: var(--color-white-at-50); + } + } +} + +/* pages/new-tab/app/customizer/VisibilityMenu.module.css */ +.VisibilityMenu_dropdownInner { + background: var(--ntp-surface-background-color); + border: 1px solid var(--color-black-at-9); + @media screen and (prefers-color-scheme: dark) { + border-color: var(--color-white-at-9); + } + box-shadow: 0 2px 6px rgba(0, 0, 0, .1), 0 8px 16px rgba(0, 0, 0, .08); + padding: var(--sp-1); + border-radius: 8px; +} +.VisibilityMenu_list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--sp-1); +} +.VisibilityMenu_menuItemLabel { + display: flex; + align-items: center; + gap: 10px; + white-space: nowrap; + font-size: var(--title-3-em-font-size); + height: 28px; + padding-left: 10px; + padding-right: 10px; + > * { + min-width: 0; + } +} +.VisibilityMenu_svg { + flex-shrink: 0; + width: 16px; + height: 16px; + display: flex; +} +.VisibilityMenu_checkbox { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.VisibilityMenu_checkboxIcon { + font-size: 0; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--color-black-at-9); + @media screen and (prefers-color-scheme: dark) { + border: 1px solid var(--color-white-at-9); + } +} +.VisibilityMenu_menuItemLabel input:checked + .VisibilityMenu_checkboxIcon { + background: var(--color-blue-50); + border-color: var(--color-blue-50); +} +.VisibilityMenu_menuItemLabel input:checked + .VisibilityMenu_checkboxIcon svg path { + stroke: white; +} +.VisibilityMenu_menuItemLabel input:focus-visible + .VisibilityMenu_checkboxIcon { + outline: dotted 1px var(--ntp-focus-outline-color); + outline-offset: 2px; +} /* pages/onboarding/app/components/Stack.module.css */ .Stack_stack { @@ -496,24 +633,6 @@ body { } } -/* pages/new-tab/app/customizer/Customizer.module.css */ -.Customizer_root { - position: fixed; - bottom: 0; - padding: 1rem; -} -.Customizer_list { - display: grid; - grid-row-gap: .5rem; -} -.Customizer_item { -} -.Customizer_label { - display: grid; - grid-template-columns: max-content max-content; - grid-column-gap: 1rem; -} - /* shared/components/Fallback/Fallback.module.css */ .Fallback_fallback { height: 100%; diff --git a/build/integration/pages/new-tab/js/index.js b/build/integration/pages/new-tab/js/index.js index 91b9b9557..98007994b 100644 --- a/build/integration/pages/new-tab/js/index.js +++ b/build/integration/pages/new-tab/js/index.js @@ -1,5 +1,82 @@ "use strict"; (() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2, + mod + )); + + // ../node_modules/classnames/index.js + var require_classnames = __commonJS({ + "../node_modules/classnames/index.js"(exports, module) { + (function() { + "use strict"; + var hasOwn = {}.hasOwnProperty; + var nativeCodeString = "[native code]"; + function classNames() { + var classes = []; + for (var i4 = 0; i4 < arguments.length; i4++) { + var arg = arguments[i4]; + if (!arg) + continue; + var argType = typeof arg; + if (argType === "string" || argType === "number") { + classes.push(arg); + } else if (Array.isArray(arg)) { + if (arg.length) { + var inner = classNames.apply(null, arg); + if (inner) { + classes.push(inner); + } + } + } else if (argType === "object") { + if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) { + classes.push(arg.toString()); + continue; + } + for (var key in arg) { + if (hasOwn.call(arg, key) && arg[key]) { + classes.push(key); + } + } + } + } + return classes.join(" "); + } + if (typeof module !== "undefined" && module.exports) { + classNames.default = classNames; + module.exports = classNames; + } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) { + define("classnames", [], function() { + return classNames; + }); + } else { + window.classNames = classNames; + } + })(); + } + }); + // ../node_modules/preact/dist/preact.module.js var n; var l; @@ -551,7 +628,7 @@ // this field is static for the lifespan of the page widgets: props.widgets, // this will be updated via subscriptions - widgetConfigItems: data, + widgetConfigItems: data || [], toggle } }, props.children); } @@ -656,8 +733,12 @@ } ] }, + widgets_visibility_menu_title: { + title: "Customize New Tab Page", + note: "Heading text describing that there's a list of toggles for customizing the page layout." + }, trackerStatsMenuTitle: { - title: "Blocked Tracking Attempts", + title: "Privacy Stats", note: "Used as a toggle label in a page customization menu" }, trackerStatsNoActivity: { @@ -699,6 +780,10 @@ favorites_show_more: { title: "Show more ({count} remaining)", note: "" + }, + favorites_menu_title: { + title: "Favorites", + note: "Used as a toggle label in a page customization menu" } }; @@ -1090,7 +1175,8 @@ var Icons_default = { chevron: "Icons_chevron", chevronCircle: "Icons_chevronCircle", - chevronArrow: "Icons_chevronArrow" + chevronArrow: "Icons_chevronArrow", + customize: "Icons_customize" }; // pages/new-tab/app/components/Icons.js @@ -1105,6 +1191,46 @@ } )); } + function CustomizeIcon() { + return /* @__PURE__ */ _("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", class: Icons_default.customize }, /* @__PURE__ */ _( + "path", + { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + d: "M4.5 1C2.567 1 1 2.567 1 4.5C1 6.433 2.567 8 4.5 8C6.17556 8 7.57612 6.82259 7.91946 5.25H14.375C14.7202 5.25 15 4.97018 15 4.625C15 4.27982 14.7202 4 14.375 4H7.96456C7.72194 2.30385 6.26324 1 4.5 1ZM2.25 4.5C2.25 3.25736 3.25736 2.25 4.5 2.25C5.74264 2.25 6.75 3.25736 6.75 4.5C6.75 5.74264 5.74264 6.75 4.5 6.75C3.25736 6.75 2.25 5.74264 2.25 4.5Z", + fill: "currentColor" + } + ), /* @__PURE__ */ _( + "path", + { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + d: "M8.03544 12H1.625C1.27982 12 1 11.7202 1 11.375C1 11.0298 1.27982 10.75 1.625 10.75H8.08054C8.42388 9.17741 9.82444 8 11.5 8C13.433 8 15 9.567 15 11.5C15 13.433 13.433 15 11.5 15C9.73676 15 8.27806 13.6961 8.03544 12ZM9.25 11.5C9.25 10.2574 10.2574 9.25 11.5 9.25C12.7426 9.25 13.75 10.2574 13.75 11.5C13.75 12.7426 12.7426 13.75 11.5 13.75C10.2574 13.75 9.25 12.7426 9.25 11.5Z", + fill: "currentColor" + } + )); + } + function DuckFoot() { + return /* @__PURE__ */ _("svg", { viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16" }, /* @__PURE__ */ _( + "path", + { + "clip-rule": "evenodd", + fill: "currentColor", + d: "M6.483.612A2.13 2.13 0 0 1 7.998 0c.56.001 1.115.215 1.512.62.673.685 1.26 1.045 1.852 1.228.594.185 1.31.228 2.311.1a2.175 2.175 0 0 1 1.575.406c.452.34.746.862.75 1.445.033 3.782-.518 6.251-1.714 8.04-1.259 1.882-3.132 2.831-5.045 3.8l-.123.063-.003.001-.125.063a2.206 2.206 0 0 1-1.976 0l-.124-.063-.003-.001-.124-.063c-1.913-.969-3.786-1.918-5.045-3.8C.52 10.05-.031 7.58 0 3.798a1.83 1.83 0 0 1 .75-1.444 2.175 2.175 0 0 1 1.573-.407c1.007.127 1.725.076 2.32-.114.59-.189 1.172-.551 1.839-1.222Zm2.267 1.36v12.233c1.872-.952 3.311-1.741 4.287-3.2.949-1.42 1.493-3.529 1.462-7.194 0-.072-.037-.17-.152-.257a.677.677 0 0 0-.484-.118c-1.126.144-2.075.115-2.945-.155-.77-.239-1.47-.664-2.168-1.309Zm-1.5 12.233V1.955c-.69.635-1.383 1.063-2.15 1.308-.87.278-1.823.317-2.963.174a.677.677 0 0 0-.484.117c-.115.087-.151.186-.152.258-.03 3.664.513 5.774 1.462 7.192.976 1.46 2.415 2.249 4.287 3.201Z" + } + )); + } + function Shield() { + return /* @__PURE__ */ _("svg", { width: "16", height: "16", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ _( + "path", + { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + d: "M6.341 1.367c.679-1.375 2.64-1.375 3.318 0l1.366 2.767a.35.35 0 0 0 .264.192l3.054.444c1.517.22 2.123 2.085 1.025 3.155l-2.21 2.155a.35.35 0 0 0-.1.31l.521 3.041c.26 1.512-1.327 2.664-2.684 1.95l-2.732-1.436a.35.35 0 0 0-.326 0l-2.732 1.437c-1.357.713-2.943-.44-2.684-1.95l.522-3.043a.35.35 0 0 0-.1-.31L.631 7.926C-.466 6.855.14 4.99 1.657 4.77l3.055-.444a.35.35 0 0 0 .263-.192l1.366-2.767Zm1.973.664a.35.35 0 0 0-.628 0L6.32 4.798A1.85 1.85 0 0 1 4.927 5.81l-3.054.444a.35.35 0 0 0-.194.597l2.21 2.154a1.85 1.85 0 0 1 .532 1.638L3.9 13.685a.35.35 0 0 0 .508.369l2.732-1.436a1.85 1.85 0 0 1 1.722 0l2.732 1.436a.35.35 0 0 0 .508-.369l-.522-3.042a1.85 1.85 0 0 1 .532-1.638l2.21-2.154a.35.35 0 0 0-.194-.597l-3.054-.444A1.85 1.85 0 0 1 9.68 4.798L8.314 2.031Z", + fill: "currentColor" + } + )); + } // pages/new-tab/app/components/ShowHideButton.jsx function ShowHideButton({ text, onClick, buttonAttrs = {} }) { @@ -1120,6 +1246,186 @@ ); } + // pages/new-tab/app/customizer/Customizer.module.css + var Customizer_default = { + root: "Customizer_root", + lowerRightFixed: "Customizer_lowerRightFixed", + dropdownMenu: "Customizer_dropdownMenu", + show: "Customizer_show", + customizeButton: "Customizer_customizeButton" + }; + + // pages/new-tab/app/customizer/VisibilityMenu.module.css + var VisibilityMenu_default = { + dropdownInner: "VisibilityMenu_dropdownInner", + list: "VisibilityMenu_list", + menuItemLabel: "VisibilityMenu_menuItemLabel", + svg: "VisibilityMenu_svg", + checkbox: "VisibilityMenu_checkbox", + checkboxIcon: "VisibilityMenu_checkboxIcon" + }; + + // pages/new-tab/app/customizer/VisibilityMenu.js + function VisibilityMenu({ rows, state, toggle }) { + const { t: t3 } = useTypedTranslation(); + const MENU_ID = g2(); + return /* @__PURE__ */ _("div", { className: VisibilityMenu_default.dropdownInner }, /* @__PURE__ */ _("h2", { className: "sr-only" }, t3("widgets_visibility_menu_title")), /* @__PURE__ */ _("ul", { className: VisibilityMenu_default.list }, rows.map((row, index) => { + const current = state[index]; + return /* @__PURE__ */ _("li", { key: row.id }, /* @__PURE__ */ _("label", { className: VisibilityMenu_default.menuItemLabel, htmlFor: MENU_ID + row.id }, /* @__PURE__ */ _( + "input", + { + type: "checkbox", + checked: current.checked, + onChange: () => toggle(row.id), + id: MENU_ID + row.id, + class: VisibilityMenu_default.checkbox + } + ), /* @__PURE__ */ _("span", { "aria-hidden": true, className: VisibilityMenu_default.checkboxIcon }, current.checked && /* @__PURE__ */ _( + "svg", + { + width: "16", + height: "16", + viewBox: "0 0 16 16", + fill: "none", + xmlns: "http://www.w3.org/2000/svg" + }, + /* @__PURE__ */ _( + "path", + { + d: "M3.5 9L6 11.5L12.5 5", + stroke: "white", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + } + ) + )), /* @__PURE__ */ _("span", { className: VisibilityMenu_default.svg }, row.icon === "shield" && /* @__PURE__ */ _(DuckFoot, null), row.icon === "star" && /* @__PURE__ */ _(Shield, null)), /* @__PURE__ */ _("span", null, row.title ?? row.id))); + }))); + } + + // pages/new-tab/app/customizer/Customizer.js + var import_classnames = __toESM(require_classnames(), 1); + function Customizer() { + const { widgetConfigItems, toggle } = x2(WidgetConfigContext); + const { setIsOpen, buttonRef, dropdownRef, isOpen } = useDropdown(); + const [rowData, setRowData] = h2( + /** @type {VisibilityRowData[]} */ + [] + ); + const toggleMenu = q2(() => { + if (isOpen) + return setIsOpen(false); + const next = []; + const detail = { + register: (incoming) => { + next.push(structuredClone(incoming)); + } + }; + const event = new CustomEvent(Customizer.OPEN_EVENT, { detail }); + window.dispatchEvent(event); + setRowData(next); + setIsOpen(true); + }, [isOpen]); + const visibilityState = rowData.map((row) => { + const item = widgetConfigItems.find((w3) => w3.id === row.id); + if (!item) + console.warn("could not find", row.id); + return { + checked: item?.visibility === "visible" + }; + }); + const MENU_ID = g2(); + const BUTTON_ID = g2(); + return /* @__PURE__ */ _("div", { class: Customizer_default.root, ref: dropdownRef }, /* @__PURE__ */ _( + CustomizerButton, + { + buttonId: BUTTON_ID, + menuId: MENU_ID, + toggleMenu, + buttonRef, + isOpen + } + ), /* @__PURE__ */ _( + "div", + { + id: MENU_ID, + class: (0, import_classnames.default)(Customizer_default.dropdownMenu, { [Customizer_default.show]: isOpen }), + "aria-labelledby": BUTTON_ID + }, + /* @__PURE__ */ _( + VisibilityMenu, + { + rows: rowData, + state: visibilityState, + toggle + } + ) + )); + } + Customizer.OPEN_EVENT = "ntp-customizer-open"; + function CustomizerButton({ menuId, buttonId, isOpen, toggleMenu, buttonRef }) { + return /* @__PURE__ */ _( + "button", + { + ref: buttonRef, + className: Customizer_default.customizeButton, + onClick: toggleMenu, + "aria-haspopup": "true", + "aria-expanded": isOpen, + "aria-controls": menuId, + id: buttonId + }, + /* @__PURE__ */ _(CustomizeIcon, null), + /* @__PURE__ */ _("span", null, "Customize") + ); + } + function CustomizerMenuPositionedFixed({ children }) { + return /* @__PURE__ */ _("div", { class: Customizer_default.lowerRightFixed }, children); + } + function useDropdown() { + const dropdownRef = A2(null); + const buttonRef = A2(null); + const [isOpen, setIsOpen] = h2(false); + y2(() => { + if (!isOpen) + return; + const handleFocusOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target) && !buttonRef.current?.contains(event.target)) { + setIsOpen(false); + } + }; + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains?.(event.target)) { + setIsOpen(false); + } + }; + const handleKeyDown = (event) => { + if (event.key === "Escape") { + setIsOpen(false); + buttonRef.current?.focus?.(); + } + }; + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("focusin", handleFocusOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("focusin", handleFocusOutside); + }; + }, [isOpen]); + return { dropdownRef, buttonRef, isOpen, setIsOpen }; + } + function useCustomizer({ title, id, icon }) { + y2(() => { + const handler = (e3) => { + e3.detail.register({ title, id, icon }); + }; + window.addEventListener(Customizer.OPEN_EVENT, handler); + return () => window.removeEventListener(Customizer.OPEN_EVENT, handler); + }, [title, id, icon]); + } + // pages/new-tab/app/privacy-stats/PrivacyStats.js function PrivacyStats({ expansion, data, toggle, animation = "auto-animate" }) { if (animation === "view-transitions") { @@ -1189,7 +1495,10 @@ })); } function PrivacyStatsCustomized() { - const { visibility } = useVisibility(); + const { t: t3 } = useTypedTranslation(); + const { visibility, id } = useVisibility(); + const title = t3("trackerStatsMenuTitle"); + useCustomizer({ title, id, icon: "star" }); if (visibility === "hidden") { return null; } @@ -1256,7 +1565,10 @@ // pages/new-tab/app/favorites/Favorites.js function FavoritesCustomized() { + const { t: t3 } = useTypedTranslation(); const { id, visibility } = useVisibility(); + const title = t3("favorites_menu_title"); + useCustomizer({ title, id, icon: "shield" }); if (visibility === "hidden") { return null; } @@ -1825,35 +2137,6 @@ 3: "var(--sp-3)" }; - // pages/new-tab/app/customizer/Customizer.module.css - var Customizer_default = { - root: "Customizer_root", - list: "Customizer_list", - item: "Customizer_item", - label: "Customizer_label" - }; - - // pages/new-tab/app/customizer/Customizer.js - function Customizer() { - const { widgets, widgetConfigItems, toggle } = x2(WidgetConfigContext); - return /* @__PURE__ */ _("div", { class: Customizer_default.root }, /* @__PURE__ */ _("ul", { class: Customizer_default.list }, widgets.map((widget) => { - const matchingConfig = widgetConfigItems.find((item) => item.id === widget.id); - if (!matchingConfig) { - console.warn("missing config for widget: ", widget); - return null; - } - return /* @__PURE__ */ _("li", { key: widget.id, class: Customizer_default.item }, /* @__PURE__ */ _("label", { class: Customizer_default.label }, /* @__PURE__ */ _( - "input", - { - type: "checkbox", - checked: matchingConfig.visibility === "visible", - onChange: () => toggle(widget.id), - value: widget.id - } - ), /* @__PURE__ */ _("span", null, widget.id))); - }))); - } - // pages/new-tab/app/widget-list/WidgetList.js var widgetMap = { privacyStats: () => /* @__PURE__ */ _(PrivacyStatsCustomized, null), @@ -1875,7 +2158,7 @@ }, widgetMap[widget.id]?.() )); - }), /* @__PURE__ */ _(Customizer, null)); + }), /* @__PURE__ */ _(CustomizerMenuPositionedFixed, null, /* @__PURE__ */ _(Customizer, null))); } // pages/new-tab/app/components/App.js @@ -2178,8 +2461,35 @@ }, /* @__PURE__ */ _(PrivacyStatsConsumer, null) ) + }, + "customizer-menu": { + factory: () => /* @__PURE__ */ _(b, null, /* @__PURE__ */ _("div", null, /* @__PURE__ */ _(CustomizerButton, { isOpen: true })), /* @__PURE__ */ _("br", null), /* @__PURE__ */ _(MaxContent, null, /* @__PURE__ */ _( + VisibilityMenu, + { + toggle: noop("toggle!"), + rows: [ + { + id: "favorites", + title: "Favorites", + icon: "star" + }, + { + id: "privacyStats", + title: "Privacy Stats", + icon: "shield" + } + ], + state: [ + { checked: true }, + { checked: false } + ] + } + ))) } }; + function MaxContent({ children }) { + return /* @__PURE__ */ _("div", { style: { display: "grid", gridTemplateColumns: "max-content" } }, children); + } // pages/new-tab/app/components/Components.jsx var url = new URL(window.location.href); @@ -3662,3 +3972,12 @@ newTabMessaging.reportInitException(msg); }); })(); +/*! Bundled license information: + +classnames/index.js: + (*! + Copyright (c) 2018 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames + *) +*/ diff --git a/build/integration/pages/new-tab/locales/en/newtab.json b/build/integration/pages/new-tab/locales/en/newtab.json index 0c1173016..4bf7e7910 100644 --- a/build/integration/pages/new-tab/locales/en/newtab.json +++ b/build/integration/pages/new-tab/locales/en/newtab.json @@ -9,8 +9,12 @@ } ] }, + "widgets_visibility_menu_title": { + "title": "Customize New Tab Page", + "note": "Heading text describing that there's a list of toggles for customizing the page layout." + }, "trackerStatsMenuTitle": { - "title": "Blocked Tracking Attempts", + "title": "Privacy Stats", "note": "Used as a toggle label in a page customization menu" }, "trackerStatsNoActivity": { @@ -52,5 +56,9 @@ "favorites_show_more": { "title": "Show more ({count} remaining)", "note": "" + }, + "favorites_menu_title": { + "title": "Favorites", + "note": "Used as a toggle label in a page customization menu" } } diff --git a/build/windows/contentScope.js b/build/windows/contentScope.js index d309af446..409925195 100644 --- a/build/windows/contentScope.js +++ b/build/windows/contentScope.js @@ -11544,7 +11544,7 @@ /** * @typedef {SuccessResponse | ErrorResponse} ActionResponse * @typedef {{ result: true } | { result: false; error: string }} BooleanResult - * @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string}} Expectation + * @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string; failSilently?: boolean}} Expectation */ /** @@ -15290,6 +15290,14 @@ results.push(setValueForInput(inputElem, generateRandomInt(parseInt(element.min), parseInt(element.max)).toString())); } else if (element.type === '$generated_street_address$') { results.push(setValueForInput(inputElem, generateStreetAddress())); + + // This is a composite of existing (but separate) city and state fields + } else if (element.type === 'cityState') { + if (!Object.prototype.hasOwnProperty.call(data, 'city') || !Object.prototype.hasOwnProperty.call(data, 'state')) { + results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the keys 'city' and 'state'` }); + continue + } + results.push(setValueForInput(inputElem, data.city + ', ' + data.state)); } else { if (!Object.prototype.hasOwnProperty.call(data, element.type)) { results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the key '${element.type}'` }); @@ -15650,21 +15658,45 @@ /** * @param {Record} action - * @param {Document | HTMLElement} root - * @return {import('../types.js').ActionResponse} + * @param {Record} userData + * @param {Document} root + * @return {Promise} */ - function expectation (action, root = document) { + async function expectation (action, userData, root = document) { const results = expectMany(action.expectations, root); - const errors = results.filter(x => x.result === false).map(x => { - if ('error' in x) return x.error - return 'unknown error' - }); + // filter out good results + silent failures, leaving only fatal errors + const errors = results + .filter((x, index) => { + if (x.result === true) return false + if (action.expectations[index].failSilently) return false + return true + }).map((x) => { + return 'error' in x ? x.error : 'unknown error' + }); if (errors.length > 0) { return new ErrorResponse({ actionID: action.id, message: errors.join(', ') }) } + // only run later actions if every expectation was met + const runActions = results.every(x => x.result === true); + const secondaryErrors = []; + + if (action.actions?.length && runActions) { + for (const subAction of action.actions) { + const result = await execute(subAction, userData, root); + + if ('error' in result) { + secondaryErrors.push(result.error); + } + } + + if (secondaryErrors.length > 0) { + return new ErrorResponse({ actionID: action.id, message: secondaryErrors.join(', ') }) + } + } + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }) } @@ -15806,7 +15838,7 @@ case 'click': return click(action, data(action, inputData, 'userProfile'), root) case 'expectation': - return expectation(action, root) + return await expectation(action, data(action, inputData, 'userProfile'), root) case 'fillForm': return fillForm(action, data(action, inputData, 'extractedProfile'), root) case 'getCaptchaInfo': diff --git a/build/windows/pages/new-tab/js/index.css b/build/windows/pages/new-tab/js/index.css index 6094cae35..b2255a201 100644 --- a/build/windows/pages/new-tab/js/index.css +++ b/build/windows/pages/new-tab/js/index.css @@ -193,6 +193,17 @@ li { margin: 0; padding: 0; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} /* pages/new-tab/app/styles/ntp-theme.css */ :root { @@ -212,7 +223,7 @@ li { --ntp-focus-outline-color: black; @media (prefers-color-scheme: dark) { --ntp-background-color: var(--color-gray-85); - --ntp-surface-background-color: var(--color-black-at-18); + --ntp-surface-background-color: #2a2a2a; --ntp-surface-border-color: var(--color-black-at-6); --ntp-text-normal: white; --ntp-focus-outline-color: white; @@ -482,6 +493,132 @@ body { fill-opacity: 0.5; } } +.Icons_customize { +} + +/* pages/new-tab/app/customizer/Customizer.module.css */ +.Customizer_root { + position: relative; +} +.Customizer_lowerRightFixed { + position: fixed; + bottom: 16px; + right: 16px; +} +.Customizer_dropdownMenu { + display: none; + position: absolute; + right: 0; + bottom: calc(100% + 10px); +} +.Customizer_show { + display: block; +} +.Customizer_customizeButton { + background-color: transparent; + border: 1px solid var(--color-black-at-9); + border-radius: 6px; + padding: 8px 12px; + cursor: pointer; + display: flex; + gap: 6px; + color: var(--color-black-at-84); + @media screen and (prefers-color-scheme: dark) { + color: var(--color-white-at-80); + border-color: var(--color-white-at-9); + } + &:focus-visible { + outline: 1px dotted var(--ntp-focus-outline-color); + } + &:hover { + background-color: var(--color-black-at-6); + border-color: var(--color-black-at-18); + @media screen and (prefers-color-scheme: dark) { + background-color: var(--color-white-at-18); + border-color: var(--color-white-at-36); + } + } + &:active { + background-color: var(--color-white-at-12); + @media screen and (prefers-color-scheme: dark) { + background-color: var(--color-white-at-24); + border-color: var(--color-white-at-50); + } + } +} + +/* pages/new-tab/app/customizer/VisibilityMenu.module.css */ +.VisibilityMenu_dropdownInner { + background: var(--ntp-surface-background-color); + border: 1px solid var(--color-black-at-9); + @media screen and (prefers-color-scheme: dark) { + border-color: var(--color-white-at-9); + } + box-shadow: 0 2px 6px rgba(0, 0, 0, .1), 0 8px 16px rgba(0, 0, 0, .08); + padding: var(--sp-1); + border-radius: 8px; +} +.VisibilityMenu_list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--sp-1); +} +.VisibilityMenu_menuItemLabel { + display: flex; + align-items: center; + gap: 10px; + white-space: nowrap; + font-size: var(--title-3-em-font-size); + height: 28px; + padding-left: 10px; + padding-right: 10px; + > * { + min-width: 0; + } +} +.VisibilityMenu_svg { + flex-shrink: 0; + width: 16px; + height: 16px; + display: flex; +} +.VisibilityMenu_checkbox { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.VisibilityMenu_checkboxIcon { + font-size: 0; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--color-black-at-9); + @media screen and (prefers-color-scheme: dark) { + border: 1px solid var(--color-white-at-9); + } +} +.VisibilityMenu_menuItemLabel input:checked + .VisibilityMenu_checkboxIcon { + background: var(--color-blue-50); + border-color: var(--color-blue-50); +} +.VisibilityMenu_menuItemLabel input:checked + .VisibilityMenu_checkboxIcon svg path { + stroke: white; +} +.VisibilityMenu_menuItemLabel input:focus-visible + .VisibilityMenu_checkboxIcon { + outline: dotted 1px var(--ntp-focus-outline-color); + outline-offset: 2px; +} /* pages/onboarding/app/components/Stack.module.css */ .Stack_stack { @@ -496,24 +633,6 @@ body { } } -/* pages/new-tab/app/customizer/Customizer.module.css */ -.Customizer_root { - position: fixed; - bottom: 0; - padding: 1rem; -} -.Customizer_list { - display: grid; - grid-row-gap: .5rem; -} -.Customizer_item { -} -.Customizer_label { - display: grid; - grid-template-columns: max-content max-content; - grid-column-gap: 1rem; -} - /* shared/components/Fallback/Fallback.module.css */ .Fallback_fallback { height: 100%; diff --git a/build/windows/pages/new-tab/js/index.js b/build/windows/pages/new-tab/js/index.js index 64c4effa8..e5afac5fa 100644 --- a/build/windows/pages/new-tab/js/index.js +++ b/build/windows/pages/new-tab/js/index.js @@ -1,5 +1,82 @@ "use strict"; (() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2, + mod + )); + + // ../node_modules/classnames/index.js + var require_classnames = __commonJS({ + "../node_modules/classnames/index.js"(exports, module) { + (function() { + "use strict"; + var hasOwn = {}.hasOwnProperty; + var nativeCodeString = "[native code]"; + function classNames() { + var classes = []; + for (var i4 = 0; i4 < arguments.length; i4++) { + var arg = arguments[i4]; + if (!arg) + continue; + var argType = typeof arg; + if (argType === "string" || argType === "number") { + classes.push(arg); + } else if (Array.isArray(arg)) { + if (arg.length) { + var inner = classNames.apply(null, arg); + if (inner) { + classes.push(inner); + } + } + } else if (argType === "object") { + if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) { + classes.push(arg.toString()); + continue; + } + for (var key in arg) { + if (hasOwn.call(arg, key) && arg[key]) { + classes.push(key); + } + } + } + } + return classes.join(" "); + } + if (typeof module !== "undefined" && module.exports) { + classNames.default = classNames; + module.exports = classNames; + } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) { + define("classnames", [], function() { + return classNames; + }); + } else { + window.classNames = classNames; + } + })(); + } + }); + // ../node_modules/preact/dist/preact.module.js var n; var l; @@ -551,7 +628,7 @@ // this field is static for the lifespan of the page widgets: props.widgets, // this will be updated via subscriptions - widgetConfigItems: data, + widgetConfigItems: data || [], toggle } }, props.children); } @@ -656,8 +733,12 @@ } ] }, + widgets_visibility_menu_title: { + title: "Customize New Tab Page", + note: "Heading text describing that there's a list of toggles for customizing the page layout." + }, trackerStatsMenuTitle: { - title: "Blocked Tracking Attempts", + title: "Privacy Stats", note: "Used as a toggle label in a page customization menu" }, trackerStatsNoActivity: { @@ -699,6 +780,10 @@ favorites_show_more: { title: "Show more ({count} remaining)", note: "" + }, + favorites_menu_title: { + title: "Favorites", + note: "Used as a toggle label in a page customization menu" } }; @@ -1090,7 +1175,8 @@ var Icons_default = { chevron: "Icons_chevron", chevronCircle: "Icons_chevronCircle", - chevronArrow: "Icons_chevronArrow" + chevronArrow: "Icons_chevronArrow", + customize: "Icons_customize" }; // pages/new-tab/app/components/Icons.js @@ -1105,6 +1191,46 @@ } )); } + function CustomizeIcon() { + return /* @__PURE__ */ _("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", class: Icons_default.customize }, /* @__PURE__ */ _( + "path", + { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + d: "M4.5 1C2.567 1 1 2.567 1 4.5C1 6.433 2.567 8 4.5 8C6.17556 8 7.57612 6.82259 7.91946 5.25H14.375C14.7202 5.25 15 4.97018 15 4.625C15 4.27982 14.7202 4 14.375 4H7.96456C7.72194 2.30385 6.26324 1 4.5 1ZM2.25 4.5C2.25 3.25736 3.25736 2.25 4.5 2.25C5.74264 2.25 6.75 3.25736 6.75 4.5C6.75 5.74264 5.74264 6.75 4.5 6.75C3.25736 6.75 2.25 5.74264 2.25 4.5Z", + fill: "currentColor" + } + ), /* @__PURE__ */ _( + "path", + { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + d: "M8.03544 12H1.625C1.27982 12 1 11.7202 1 11.375C1 11.0298 1.27982 10.75 1.625 10.75H8.08054C8.42388 9.17741 9.82444 8 11.5 8C13.433 8 15 9.567 15 11.5C15 13.433 13.433 15 11.5 15C9.73676 15 8.27806 13.6961 8.03544 12ZM9.25 11.5C9.25 10.2574 10.2574 9.25 11.5 9.25C12.7426 9.25 13.75 10.2574 13.75 11.5C13.75 12.7426 12.7426 13.75 11.5 13.75C10.2574 13.75 9.25 12.7426 9.25 11.5Z", + fill: "currentColor" + } + )); + } + function DuckFoot() { + return /* @__PURE__ */ _("svg", { viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16" }, /* @__PURE__ */ _( + "path", + { + "clip-rule": "evenodd", + fill: "currentColor", + d: "M6.483.612A2.13 2.13 0 0 1 7.998 0c.56.001 1.115.215 1.512.62.673.685 1.26 1.045 1.852 1.228.594.185 1.31.228 2.311.1a2.175 2.175 0 0 1 1.575.406c.452.34.746.862.75 1.445.033 3.782-.518 6.251-1.714 8.04-1.259 1.882-3.132 2.831-5.045 3.8l-.123.063-.003.001-.125.063a2.206 2.206 0 0 1-1.976 0l-.124-.063-.003-.001-.124-.063c-1.913-.969-3.786-1.918-5.045-3.8C.52 10.05-.031 7.58 0 3.798a1.83 1.83 0 0 1 .75-1.444 2.175 2.175 0 0 1 1.573-.407c1.007.127 1.725.076 2.32-.114.59-.189 1.172-.551 1.839-1.222Zm2.267 1.36v12.233c1.872-.952 3.311-1.741 4.287-3.2.949-1.42 1.493-3.529 1.462-7.194 0-.072-.037-.17-.152-.257a.677.677 0 0 0-.484-.118c-1.126.144-2.075.115-2.945-.155-.77-.239-1.47-.664-2.168-1.309Zm-1.5 12.233V1.955c-.69.635-1.383 1.063-2.15 1.308-.87.278-1.823.317-2.963.174a.677.677 0 0 0-.484.117c-.115.087-.151.186-.152.258-.03 3.664.513 5.774 1.462 7.192.976 1.46 2.415 2.249 4.287 3.201Z" + } + )); + } + function Shield() { + return /* @__PURE__ */ _("svg", { width: "16", height: "16", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ _( + "path", + { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + d: "M6.341 1.367c.679-1.375 2.64-1.375 3.318 0l1.366 2.767a.35.35 0 0 0 .264.192l3.054.444c1.517.22 2.123 2.085 1.025 3.155l-2.21 2.155a.35.35 0 0 0-.1.31l.521 3.041c.26 1.512-1.327 2.664-2.684 1.95l-2.732-1.436a.35.35 0 0 0-.326 0l-2.732 1.437c-1.357.713-2.943-.44-2.684-1.95l.522-3.043a.35.35 0 0 0-.1-.31L.631 7.926C-.466 6.855.14 4.99 1.657 4.77l3.055-.444a.35.35 0 0 0 .263-.192l1.366-2.767Zm1.973.664a.35.35 0 0 0-.628 0L6.32 4.798A1.85 1.85 0 0 1 4.927 5.81l-3.054.444a.35.35 0 0 0-.194.597l2.21 2.154a1.85 1.85 0 0 1 .532 1.638L3.9 13.685a.35.35 0 0 0 .508.369l2.732-1.436a1.85 1.85 0 0 1 1.722 0l2.732 1.436a.35.35 0 0 0 .508-.369l-.522-3.042a1.85 1.85 0 0 1 .532-1.638l2.21-2.154a.35.35 0 0 0-.194-.597l-3.054-.444A1.85 1.85 0 0 1 9.68 4.798L8.314 2.031Z", + fill: "currentColor" + } + )); + } // pages/new-tab/app/components/ShowHideButton.jsx function ShowHideButton({ text, onClick, buttonAttrs = {} }) { @@ -1120,6 +1246,186 @@ ); } + // pages/new-tab/app/customizer/Customizer.module.css + var Customizer_default = { + root: "Customizer_root", + lowerRightFixed: "Customizer_lowerRightFixed", + dropdownMenu: "Customizer_dropdownMenu", + show: "Customizer_show", + customizeButton: "Customizer_customizeButton" + }; + + // pages/new-tab/app/customizer/VisibilityMenu.module.css + var VisibilityMenu_default = { + dropdownInner: "VisibilityMenu_dropdownInner", + list: "VisibilityMenu_list", + menuItemLabel: "VisibilityMenu_menuItemLabel", + svg: "VisibilityMenu_svg", + checkbox: "VisibilityMenu_checkbox", + checkboxIcon: "VisibilityMenu_checkboxIcon" + }; + + // pages/new-tab/app/customizer/VisibilityMenu.js + function VisibilityMenu({ rows, state, toggle }) { + const { t: t3 } = useTypedTranslation(); + const MENU_ID = g2(); + return /* @__PURE__ */ _("div", { className: VisibilityMenu_default.dropdownInner }, /* @__PURE__ */ _("h2", { className: "sr-only" }, t3("widgets_visibility_menu_title")), /* @__PURE__ */ _("ul", { className: VisibilityMenu_default.list }, rows.map((row, index) => { + const current = state[index]; + return /* @__PURE__ */ _("li", { key: row.id }, /* @__PURE__ */ _("label", { className: VisibilityMenu_default.menuItemLabel, htmlFor: MENU_ID + row.id }, /* @__PURE__ */ _( + "input", + { + type: "checkbox", + checked: current.checked, + onChange: () => toggle(row.id), + id: MENU_ID + row.id, + class: VisibilityMenu_default.checkbox + } + ), /* @__PURE__ */ _("span", { "aria-hidden": true, className: VisibilityMenu_default.checkboxIcon }, current.checked && /* @__PURE__ */ _( + "svg", + { + width: "16", + height: "16", + viewBox: "0 0 16 16", + fill: "none", + xmlns: "http://www.w3.org/2000/svg" + }, + /* @__PURE__ */ _( + "path", + { + d: "M3.5 9L6 11.5L12.5 5", + stroke: "white", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + } + ) + )), /* @__PURE__ */ _("span", { className: VisibilityMenu_default.svg }, row.icon === "shield" && /* @__PURE__ */ _(DuckFoot, null), row.icon === "star" && /* @__PURE__ */ _(Shield, null)), /* @__PURE__ */ _("span", null, row.title ?? row.id))); + }))); + } + + // pages/new-tab/app/customizer/Customizer.js + var import_classnames = __toESM(require_classnames(), 1); + function Customizer() { + const { widgetConfigItems, toggle } = x2(WidgetConfigContext); + const { setIsOpen, buttonRef, dropdownRef, isOpen } = useDropdown(); + const [rowData, setRowData] = h2( + /** @type {VisibilityRowData[]} */ + [] + ); + const toggleMenu = q2(() => { + if (isOpen) + return setIsOpen(false); + const next = []; + const detail = { + register: (incoming) => { + next.push(structuredClone(incoming)); + } + }; + const event = new CustomEvent(Customizer.OPEN_EVENT, { detail }); + window.dispatchEvent(event); + setRowData(next); + setIsOpen(true); + }, [isOpen]); + const visibilityState = rowData.map((row) => { + const item = widgetConfigItems.find((w3) => w3.id === row.id); + if (!item) + console.warn("could not find", row.id); + return { + checked: item?.visibility === "visible" + }; + }); + const MENU_ID = g2(); + const BUTTON_ID = g2(); + return /* @__PURE__ */ _("div", { class: Customizer_default.root, ref: dropdownRef }, /* @__PURE__ */ _( + CustomizerButton, + { + buttonId: BUTTON_ID, + menuId: MENU_ID, + toggleMenu, + buttonRef, + isOpen + } + ), /* @__PURE__ */ _( + "div", + { + id: MENU_ID, + class: (0, import_classnames.default)(Customizer_default.dropdownMenu, { [Customizer_default.show]: isOpen }), + "aria-labelledby": BUTTON_ID + }, + /* @__PURE__ */ _( + VisibilityMenu, + { + rows: rowData, + state: visibilityState, + toggle + } + ) + )); + } + Customizer.OPEN_EVENT = "ntp-customizer-open"; + function CustomizerButton({ menuId, buttonId, isOpen, toggleMenu, buttonRef }) { + return /* @__PURE__ */ _( + "button", + { + ref: buttonRef, + className: Customizer_default.customizeButton, + onClick: toggleMenu, + "aria-haspopup": "true", + "aria-expanded": isOpen, + "aria-controls": menuId, + id: buttonId + }, + /* @__PURE__ */ _(CustomizeIcon, null), + /* @__PURE__ */ _("span", null, "Customize") + ); + } + function CustomizerMenuPositionedFixed({ children }) { + return /* @__PURE__ */ _("div", { class: Customizer_default.lowerRightFixed }, children); + } + function useDropdown() { + const dropdownRef = A2(null); + const buttonRef = A2(null); + const [isOpen, setIsOpen] = h2(false); + y2(() => { + if (!isOpen) + return; + const handleFocusOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target) && !buttonRef.current?.contains(event.target)) { + setIsOpen(false); + } + }; + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains?.(event.target)) { + setIsOpen(false); + } + }; + const handleKeyDown = (event) => { + if (event.key === "Escape") { + setIsOpen(false); + buttonRef.current?.focus?.(); + } + }; + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("focusin", handleFocusOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("focusin", handleFocusOutside); + }; + }, [isOpen]); + return { dropdownRef, buttonRef, isOpen, setIsOpen }; + } + function useCustomizer({ title, id, icon }) { + y2(() => { + const handler = (e3) => { + e3.detail.register({ title, id, icon }); + }; + window.addEventListener(Customizer.OPEN_EVENT, handler); + return () => window.removeEventListener(Customizer.OPEN_EVENT, handler); + }, [title, id, icon]); + } + // pages/new-tab/app/privacy-stats/PrivacyStats.js function PrivacyStats({ expansion, data, toggle, animation = "auto-animate" }) { if (animation === "view-transitions") { @@ -1189,7 +1495,10 @@ })); } function PrivacyStatsCustomized() { - const { visibility } = useVisibility(); + const { t: t3 } = useTypedTranslation(); + const { visibility, id } = useVisibility(); + const title = t3("trackerStatsMenuTitle"); + useCustomizer({ title, id, icon: "star" }); if (visibility === "hidden") { return null; } @@ -1256,7 +1565,10 @@ // pages/new-tab/app/favorites/Favorites.js function FavoritesCustomized() { + const { t: t3 } = useTypedTranslation(); const { id, visibility } = useVisibility(); + const title = t3("favorites_menu_title"); + useCustomizer({ title, id, icon: "shield" }); if (visibility === "hidden") { return null; } @@ -1825,35 +2137,6 @@ 3: "var(--sp-3)" }; - // pages/new-tab/app/customizer/Customizer.module.css - var Customizer_default = { - root: "Customizer_root", - list: "Customizer_list", - item: "Customizer_item", - label: "Customizer_label" - }; - - // pages/new-tab/app/customizer/Customizer.js - function Customizer() { - const { widgets, widgetConfigItems, toggle } = x2(WidgetConfigContext); - return /* @__PURE__ */ _("div", { class: Customizer_default.root }, /* @__PURE__ */ _("ul", { class: Customizer_default.list }, widgets.map((widget) => { - const matchingConfig = widgetConfigItems.find((item) => item.id === widget.id); - if (!matchingConfig) { - console.warn("missing config for widget: ", widget); - return null; - } - return /* @__PURE__ */ _("li", { key: widget.id, class: Customizer_default.item }, /* @__PURE__ */ _("label", { class: Customizer_default.label }, /* @__PURE__ */ _( - "input", - { - type: "checkbox", - checked: matchingConfig.visibility === "visible", - onChange: () => toggle(widget.id), - value: widget.id - } - ), /* @__PURE__ */ _("span", null, widget.id))); - }))); - } - // pages/new-tab/app/widget-list/WidgetList.js var widgetMap = { privacyStats: () => /* @__PURE__ */ _(PrivacyStatsCustomized, null), @@ -1875,7 +2158,7 @@ }, widgetMap[widget.id]?.() )); - }), /* @__PURE__ */ _(Customizer, null)); + }), /* @__PURE__ */ _(CustomizerMenuPositionedFixed, null, /* @__PURE__ */ _(Customizer, null))); } // pages/new-tab/app/components/App.js @@ -2178,8 +2461,35 @@ }, /* @__PURE__ */ _(PrivacyStatsConsumer, null) ) + }, + "customizer-menu": { + factory: () => /* @__PURE__ */ _(b, null, /* @__PURE__ */ _("div", null, /* @__PURE__ */ _(CustomizerButton, { isOpen: true })), /* @__PURE__ */ _("br", null), /* @__PURE__ */ _(MaxContent, null, /* @__PURE__ */ _( + VisibilityMenu, + { + toggle: noop("toggle!"), + rows: [ + { + id: "favorites", + title: "Favorites", + icon: "star" + }, + { + id: "privacyStats", + title: "Privacy Stats", + icon: "shield" + } + ], + state: [ + { checked: true }, + { checked: false } + ] + } + ))) } }; + function MaxContent({ children }) { + return /* @__PURE__ */ _("div", { style: { display: "grid", gridTemplateColumns: "max-content" } }, children); + } // pages/new-tab/app/components/Components.jsx var url = new URL(window.location.href); @@ -3523,3 +3833,12 @@ newTabMessaging.reportInitException(msg); }); })(); +/*! Bundled license information: + +classnames/index.js: + (*! + Copyright (c) 2018 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames + *) +*/ diff --git a/build/windows/pages/new-tab/locales/en/newtab.json b/build/windows/pages/new-tab/locales/en/newtab.json index 0c1173016..4bf7e7910 100644 --- a/build/windows/pages/new-tab/locales/en/newtab.json +++ b/build/windows/pages/new-tab/locales/en/newtab.json @@ -9,8 +9,12 @@ } ] }, + "widgets_visibility_menu_title": { + "title": "Customize New Tab Page", + "note": "Heading text describing that there's a list of toggles for customizing the page layout." + }, "trackerStatsMenuTitle": { - "title": "Blocked Tracking Attempts", + "title": "Privacy Stats", "note": "Used as a toggle label in a page customization menu" }, "trackerStatsNoActivity": { @@ -52,5 +56,9 @@ "favorites_show_more": { "title": "Show more ({count} remaining)", "note": "" + }, + "favorites_menu_title": { + "title": "Favorites", + "note": "Used as a toggle label in a page customization menu" } } diff --git a/injected/integration-test/broker-protection.spec.js b/injected/integration-test/broker-protection.spec.js index 8732a1b66..293c709ce 100644 --- a/injected/integration-test/broker-protection.spec.js +++ b/injected/integration-test/broker-protection.spec.js @@ -1,4 +1,4 @@ -import { test } from '@playwright/test' +import { test, expect } from '@playwright/test' import { BrokerProtectionPage } from './page-objects/broker-protection.js' test.describe('Broker Protection communications', () => { @@ -490,6 +490,56 @@ test.describe('Broker Protection communications', () => { }) }) + test('expectation with actions', async ({ page }, workerInfo) => { + const dbp = BrokerProtectionPage.create(page, workerInfo) + await dbp.enabled() + await dbp.navigatesTo('expectation-actions.html') + await dbp.receivesAction('expectation-actions.json') + const response = await dbp.waitForMessage('actionCompleted') + + dbp.isSuccessMessage(response) + await page.waitForURL(url => url.hash === '#1', { timeout: 2000 }) + }) + + test('expectation fails when failSilently is not present', async ({ page }, workerInfo) => { + const dbp = BrokerProtectionPage.create(page, workerInfo) + await dbp.enabled() + await dbp.navigatesTo('expectation-actions.html') + await dbp.receivesAction('expectation-actions-fail.json') + + const response = await dbp.waitForMessage('actionCompleted') + dbp.isErrorMessage(response) + + const currentUrl = page.url() + expect(currentUrl).not.toContain('#') + }) + + test('expectation succeeds when failSilently is present', async ({ page }, workerInfo) => { + const dbp = BrokerProtectionPage.create(page, workerInfo) + await dbp.enabled() + await dbp.navigatesTo('expectation-actions.html') + await dbp.receivesAction('expectation-actions-fail-silently.json') + + const response = await dbp.waitForMessage('actionCompleted') + dbp.isSuccessMessage(response) + + const currentUrl = page.url() + expect(currentUrl).not.toContain('#') + }) + + test('expectation succeeds but subaction fails should throw error', async ({ page }, workerInfo) => { + const dbp = BrokerProtectionPage.create(page, workerInfo) + await dbp.enabled() + await dbp.navigatesTo('expectation-actions.html') + await dbp.receivesAction('expectation-actions-subaction-fail.json') + + const response = await dbp.waitForMessage('actionCompleted') + dbp.isErrorMessage(response) + + const currentUrl = page.url() + expect(currentUrl).not.toContain('#') + }) + test.describe('retrying', () => { test('retrying a click', async ({ page }, workerInfo) => { const dbp = BrokerProtectionPage.create(page, workerInfo) diff --git a/injected/integration-test/page-objects/broker-protection.js b/injected/integration-test/page-objects/broker-protection.js index 0ee10d6a6..ac0f952d8 100644 --- a/injected/integration-test/page-objects/broker-protection.js +++ b/injected/integration-test/page-objects/broker-protection.js @@ -52,12 +52,12 @@ export class BrokerProtectionPage { * @return {Promise} */ async isFormFilled () { - await expect(this.page.getByLabel('First Name:')).toHaveValue('John') - await expect(this.page.getByLabel('Last Name:')).toHaveValue('Smith') - await expect(this.page.getByLabel('Phone Number:')).toHaveValue(/^\d{10}$/) - await expect(this.page.getByLabel('Street Address:')).toHaveValue(/^\d+ [A-Za-z]+(?: [A-Za-z]+)?$/) - await expect(this.page.getByLabel('State:')).toHaveValue('IL') - await expect(this.page.getByLabel('Zip Code:')).toHaveValue(/^\d{5}$/) + await expect(this.page.getByLabel('First Name:', { exact: true })).toHaveValue('John') + await expect(this.page.getByLabel('Last Name:', { exact: true })).toHaveValue('Smith') + await expect(this.page.getByLabel('Phone Number:', { exact: true })).toHaveValue(/^\d{10}$/) + await expect(this.page.getByLabel('Street Address:', { exact: true })).toHaveValue(/^\d+ [A-Za-z]+(?: [A-Za-z]+)?$/) + await expect(this.page.locator('#state')).toHaveValue('IL') + await expect(this.page.getByLabel('Zip Code:', { exact: true })).toHaveValue(/^\d{5}$/) const randomValue = await this.page.getByLabel('Random number between 5 and 15:').inputValue() const randomValueInt = parseInt(randomValue) @@ -65,6 +65,8 @@ export class BrokerProtectionPage { expect(Number.isInteger(randomValueInt)).toBe(true) expect(randomValueInt).toBeGreaterThanOrEqual(5) expect(randomValueInt).toBeLessThanOrEqual(15) + + await expect(this.page.getByLabel('City & State:', { exact: true })).toHaveValue('Chicago, IL') } /** diff --git a/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-fail-silently.json b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-fail-silently.json new file mode 100644 index 000000000..e662cfbb8 --- /dev/null +++ b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-fail-silently.json @@ -0,0 +1,27 @@ +{ + "state": { + "action": { + "actionType": "expectation", + "id": "2", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "How old is Jane Doe?", + "failSilently": true + } + ], + "actions": [ + { + "actionType": "click", + "elements": [ + { + "type": "button", + "selector": ".view-more" + } + ] + } + ] + } + } + } \ No newline at end of file diff --git a/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-fail.json b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-fail.json new file mode 100644 index 000000000..f32cf8b1e --- /dev/null +++ b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-fail.json @@ -0,0 +1,31 @@ +{ + "state": { + "action": { + "actionType": "expectation", + "id": "2", + "retry": { + "environment": "web", + "interval": { "ms": 1000 }, + "maxAttempts": 1 + }, + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "How old is Jane Doe?" + } + ], + "actions": [ + { + "actionType": "click", + "elements": [ + { + "type": "button", + "selector": ".view-more" + } + ] + } + ] + } + } + } \ No newline at end of file diff --git a/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-subaction-fail.json b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-subaction-fail.json new file mode 100644 index 000000000..c2368ecea --- /dev/null +++ b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions-subaction-fail.json @@ -0,0 +1,31 @@ +{ + "state": { + "action": { + "actionType": "expectation", + "id": "2", + "retry": { + "environment": "web", + "interval": { "ms": 1000 }, + "maxAttempts": 1 + }, + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "How old is John Doe?" + } + ], + "actions": [ + { + "actionType": "click", + "elements": [ + { + "type": "button", + "selector": ".view-less" + } + ] + } + ] + } + } + } \ No newline at end of file diff --git a/injected/integration-test/test-pages/broker-protection/actions/expectation-actions.json b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions.json new file mode 100644 index 000000000..9ca8a3f0b --- /dev/null +++ b/injected/integration-test/test-pages/broker-protection/actions/expectation-actions.json @@ -0,0 +1,26 @@ +{ + "state": { + "action": { + "actionType": "expectation", + "id": "2", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "How old is John Doe?" + } + ], + "actions": [ + { + "actionType": "click", + "elements": [ + { + "type": "button", + "selector": ".view-more" + } + ] + } + ] + } + } + } \ No newline at end of file diff --git a/injected/integration-test/test-pages/broker-protection/actions/fill-form.json b/injected/integration-test/test-pages/broker-protection/actions/fill-form.json index 7d03f83ef..711aaa31f 100644 --- a/injected/integration-test/test-pages/broker-protection/actions/fill-form.json +++ b/injected/integration-test/test-pages/broker-protection/actions/fill-form.json @@ -34,6 +34,10 @@ "selector": "#random_number", "min": "5", "max": "15" + }, + { + "type": "cityState", + "selector": "#city_state" } ] }, diff --git a/injected/integration-test/test-pages/broker-protection/pages/expectation-actions.html b/injected/integration-test/test-pages/broker-protection/pages/expectation-actions.html new file mode 100644 index 000000000..7b5aaa1b4 --- /dev/null +++ b/injected/integration-test/test-pages/broker-protection/pages/expectation-actions.html @@ -0,0 +1,23 @@ + + + + + + Broker Protection + + +
+ How old is John Doe? + View More +
+ + + \ No newline at end of file diff --git a/injected/integration-test/test-pages/broker-protection/pages/form.html b/injected/integration-test/test-pages/broker-protection/pages/form.html index 94e8d882f..81a719c1f 100644 --- a/injected/integration-test/test-pages/broker-protection/pages/form.html +++ b/injected/integration-test/test-pages/broker-protection/pages/form.html @@ -51,6 +51,10 @@ Random number between 5 and 15: +