tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 7c0dde347f042d1320875472ecac5ce5eed93bbb
parent 1822997ff30f03812397c1106ef2b67a07d382bf
Author: Meg Viar <lmegviar@gmail.com>
Date:   Fri, 24 Oct 2025 10:46:36 +0000

Bug 1981255 - Gate rendering first about:welcome screen until after screen filtering finishes r=emcminn,omc-reviewers,mimi,jprickett

Differential Revision: https://phabricator.services.mozilla.com/D269791

Diffstat:
Mbrowser/components/aboutwelcome/content-src/aboutwelcome.jsx | 1+
Mbrowser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx | 16++++++++++++++--
Mbrowser/components/aboutwelcome/content/aboutwelcome.bundle.js | 18++++++++++++++++--
Mbrowser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx | 47+++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 78 insertions(+), 4 deletions(-)

diff --git a/browser/components/aboutwelcome/content-src/aboutwelcome.jsx b/browser/components/aboutwelcome/content-src/aboutwelcome.jsx @@ -74,6 +74,7 @@ class AboutWelcome extends React.PureComponent { startScreen={props.startScreen || 0} appAndSystemLocaleInfo={props.appAndSystemLocaleInfo} ariaRole={props.aria_role} + gateInitialPaint={true} /> ); } diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx @@ -15,6 +15,7 @@ const TRANSITION_OUT_TIME = 1000; const LANGUAGE_MISMATCH_SCREEN_ID = "AW_LANGUAGE_MISMATCH"; export const MultiStageAboutWelcome = props => { + const gateInitialPaint = props.gateInitialPaint ?? false; let { defaultScreens } = props; const didFilter = useRef(false); const [didMount, setDidMount] = useState(false); @@ -22,6 +23,8 @@ export const MultiStageAboutWelcome = props => { const [index, setScreenIndex] = useState(props.startScreen); const [previousOrder, setPreviousOrder] = useState(props.startScreen - 1); + // Gate first paint until we've finished the initial filtering pass. + const [ready, setReady] = useState(false); useEffect(() => { (async () => { @@ -56,8 +59,11 @@ export const MultiStageAboutWelcome = props => { filtered => screens.find(s => s.id === filtered.id) ?? filtered ) ); - - didFilter.current = true; + // Mark the initial filter pass complete and allow the first paint. + if (!didFilter.current) { + didFilter.current = true; + setReady(true); + } // After completing screen filtering, trigger any unhandled campaign // action present in the attribution campaign data. This updates the @@ -241,6 +247,12 @@ export const MultiStageAboutWelcome = props => { })(); }, [index]); + // Do not render anything until the first filtering pass completes if gating + // initial paint is enabled. + if (gateInitialPaint && !ready) { + return null; + } + return ( <React.Fragment> <div diff --git a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js @@ -168,6 +168,7 @@ __webpack_require__.r(__webpack_exports__); const TRANSITION_OUT_TIME = 1000; const LANGUAGE_MISMATCH_SCREEN_ID = "AW_LANGUAGE_MISMATCH"; const MultiStageAboutWelcome = props => { + const gateInitialPaint = props.gateInitialPaint ?? false; let { defaultScreens } = props; @@ -176,6 +177,8 @@ const MultiStageAboutWelcome = props => { const [screens, setScreens] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(defaultScreens); const [index, setScreenIndex] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(props.startScreen); const [previousOrder, setPreviousOrder] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(props.startScreen - 1); + // Gate first paint until we've finished the initial filtering pass. + const [ready, setReady] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false); (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => { (async () => { // If we want to load index from history state, we don't want to send impression yet @@ -194,7 +197,11 @@ const MultiStageAboutWelcome = props => { // Use existing screen for the filtered screen to carry over any modification // e.g. if AW_LANGUAGE_MISMATCH exists, use it from existing screens setScreens(filteredScreens.map(filtered => screens.find(s => s.id === filtered.id) ?? filtered)); - didFilter.current = true; + // Mark the initial filter pass complete and allow the first paint. + if (!didFilter.current) { + didFilter.current = true; + setReady(true); + } // After completing screen filtering, trigger any unhandled campaign // action present in the attribution campaign data. This updates the @@ -361,6 +368,12 @@ const MultiStageAboutWelcome = props => { setInstalledAddons(addons); })(); }, [index]); + + // Do not render anything until the first filtering pass completes if gating + // initial paint is enabled. + if (gateInitialPaint && !ready) { + return null; + } return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `outer-wrapper onboardingContainer proton transition-${transition}`, style: props.backdrop ? { @@ -4031,7 +4044,8 @@ class AboutWelcome extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom backdrop: props.backdrop, startScreen: props.startScreen || 0, appAndSystemLocaleInfo: props.appAndSystemLocaleInfo, - ariaRole: props.aria_role + ariaRole: props.aria_role, + gateInitialPaint: true }); } } diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx @@ -218,6 +218,53 @@ describe("MultiStageAboutWelcome module", () => { assert.equal(stub.lastCall.args[2], "SELECT_CHECKBOX"); stub.restore(); }); + + it("does not render anything until targeting/filtering resolves (gated first paint)", async () => { + let resolveTargeting; + const targetingPromise = new Promise(r => (resolveTargeting = r)); + globals.set("AWEvaluateScreenTargeting", () => targetingPromise); + + const wrapper = mount( + <MultiStageAboutWelcome {...DEFAULT_PROPS} gateInitialPaint={true} /> + ); + // Immediately after mount (before resolve), nothing should be painted + assert.strictEqual(wrapper.html(), null); + assert.ok(!wrapper.find(WelcomeScreen).exists()); + + resolveTargeting(); + await spinEventLoop(); + wrapper.update(); + + // After targeting resolves, the first screen should render + assert.ok(wrapper.find(WelcomeScreen).exists()); + }); + + it("does not send telemetry before first paint is ready, but does afterwards", async () => { + let resolveTargeting; + const targetingPromise = new Promise(r => (resolveTargeting = r)); + globals.set("AWEvaluateScreenTargeting", () => targetingPromise); + + const sendEventStub = sinon.stub(global, "AWSendEventTelemetry"); + const wrapper = mount( + <MultiStageAboutWelcome {...DEFAULT_PROPS} gateInitialPaint={true} /> + ); + assert.notCalled(sendEventStub); + assert.strictEqual(wrapper.html(), null); + + resolveTargeting(); + await spinEventLoop(); + wrapper.update(); + + const hadImpression = sendEventStub + .getCalls() + .some(c => c.args && c.args[0] && c.args[0].event === "IMPRESSION"); + assert.ok( + hadImpression, + "Expected at least one IMPRESSION event after paint" + ); + + sendEventStub.restore(); + }); }); describe("WelcomeScreen component", () => {