Skip to content

Commit a890ff8

Browse files
authored
Support forced zoom in webcompat feature (#877)
* Support forced zoom in webcompat feature * Style PR fixes * Mark parsed viewport as readonly to avoid getting out of sync * Fix tests
1 parent 3e9a442 commit a890ff8

File tree

3 files changed

+82
-16
lines changed

3 files changed

+82
-16
lines changed

integration-test/test-web-compat.js

+42-4
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,18 @@ describe('Viewport fixes', () => {
755755
expect(viewportValue).toBeUndefined()
756756
})
757757

758+
it('should respect forced zoom', async () => {
759+
await gotoAndWait(page, `http://localhost:${port}/blank.html`, {
760+
site: { enabledFeatures: ['webCompat'] },
761+
featureSettings: { webCompat: { viewportWidth: 'enabled' } },
762+
desktopModeEnabled: false,
763+
forcedZoomEnabled: true
764+
}, 'document.head.innerHTML += \'<meta name="viewport" content="width=device-width">\'')
765+
766+
const viewportValue = await page.evaluate(getViewportValue)
767+
expect(viewportValue).toEqual('initial-scale=1, user-scalable=yes, maximum-scale=10, width=device-width')
768+
})
769+
758770
describe('Desktop mode off', () => {
759771
it('should respect the forcedMobileValue config', async () => {
760772
await gotoAndWait(page, `http://localhost:${port}/blank.html`, {
@@ -775,7 +787,20 @@ describe('Viewport fixes', () => {
775787
const width = await page.evaluate('screen.width')
776788
const expectedWidth = width < 1280 ? 980 : 1280
777789
const viewportValue = await page.evaluate(getViewportValue)
778-
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}`)
790+
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes`)
791+
})
792+
793+
it('should respect forced zoom', async () => {
794+
await gotoAndWait(page, `http://localhost:${port}/blank.html`, {
795+
site: { enabledFeatures: ['webCompat'] },
796+
featureSettings: { webCompat: { viewportWidth: 'enabled' } },
797+
desktopModeEnabled: false,
798+
forcedZoomEnabled: true
799+
})
800+
const width = await page.evaluate('screen.width')
801+
const expectedWidth = width < 1280 ? 980 : 1280
802+
const viewportValue = await page.evaluate(getViewportValue)
803+
expect(viewportValue).toEqual(`initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, maximum-scale=10, width=${expectedWidth}`)
779804
})
780805

781806
it('should fix the WebView edge case', async () => {
@@ -819,7 +844,7 @@ describe('Viewport fixes', () => {
819844
const width = await page.evaluate('screen.width')
820845
const expectedWidth = width < 1280 ? 980 : 1280
821846
const viewportValue = await page.evaluate(getViewportValue)
822-
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, something-something`)
847+
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`)
823848
})
824849

825850
it('should force wide viewport, ignoring the viewport tag 2', async () => {
@@ -831,7 +856,20 @@ describe('Viewport fixes', () => {
831856
const width = await page.evaluate('screen.width')
832857
const expectedWidth = width < 1280 ? 980 : 1280
833858
const viewportValue = await page.evaluate(getViewportValue)
834-
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)},something-something`)
859+
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`)
860+
})
861+
862+
it('should respect forced zoom', async () => {
863+
await gotoAndWait(page, `http://localhost:${port}/blank.html`, {
864+
site: { enabledFeatures: ['webCompat'] },
865+
featureSettings: { webCompat: { viewportWidth: 'enabled' } },
866+
desktopModeEnabled: true,
867+
forcedZoomEnabled: true
868+
}, 'document.head.innerHTML += \'<meta name="viewport" content="width=device-width, initial-scale=2, user-scalable=no, something-something">\'')
869+
const width = await page.evaluate('screen.width')
870+
const expectedWidth = width < 1280 ? 980 : 1280
871+
const viewportValue = await page.evaluate(getViewportValue)
872+
expect(viewportValue).toEqual(`initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, maximum-scale=10, width=${expectedWidth}, something-something`)
835873
})
836874

837875
it('should ignore the character case in the viewport tag', async () => {
@@ -843,7 +881,7 @@ describe('Viewport fixes', () => {
843881
const width = await page.evaluate('screen.width')
844882
const expectedWidth = width < 1280 ? 980 : 1280
845883
const viewportValue = await page.evaluate(getViewportValue)
846-
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, something-something`)
884+
expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`)
847885
})
848886
})
849887
})

src/content-feature.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default class ContentFeature {
3838
/** @type {boolean} */
3939
#isDebugFlagSet = false
4040

41-
/** @type {{ debug?: boolean, desktopModeEnabled?: boolean, featureSettings?: Record<string, unknown>, assets?: AssetConfig | undefined, site: Site, messagingConfig?: import('@duckduckgo/messaging').MessagingConfig } | null} */
41+
/** @type {{ debug?: boolean, desktopModeEnabled?: boolean, forcedZoomEnabled?: boolean, featureSettings?: Record<string, unknown>, assets?: AssetConfig | undefined, site: Site, messagingConfig?: import('@duckduckgo/messaging').MessagingConfig } | null} */
4242
#args
4343

4444
constructor (featureName) {
@@ -55,6 +55,10 @@ export default class ContentFeature {
5555
return this.#args?.desktopModeEnabled || false
5656
}
5757

58+
get forcedZoomEnabled () {
59+
return this.#args?.forcedZoomEnabled || false
60+
}
61+
5862
/**
5963
* @param {import('./utils').Platform} platform
6064
*/

src/features/web-compat.js

+35-11
Original file line numberDiff line numberDiff line change
@@ -603,30 +603,40 @@ export default class WebCompat extends ContentFeature {
603603
// Chrome respects only the last viewport tag
604604
const viewportTag = viewportTags.length === 0 ? null : viewportTags[viewportTags.length - 1]
605605
const viewportContent = viewportTag?.getAttribute('content') || ''
606+
/** @type {readonly string[]} **/
606607
const viewportContentParts = viewportContent ? viewportContent.split(/,|;/) : []
608+
/** @type {readonly string[][]} **/
607609
const parsedViewportContent = viewportContentParts.map((part) => {
608610
const [key, value] = part.split('=').map(p => p.trim().toLowerCase())
609611
return [key, value]
610612
})
611613

614+
// first, check if there are any forced values
612615
const { forcedDesktopValue, forcedMobileValue } = this.getFeatureSetting('viewportWidth')
613616
if (typeof forcedDesktopValue === 'string' && this.desktopModeEnabled) {
614617
this.forceViewportTag(viewportTag, forcedDesktopValue)
618+
return
615619
} else if (typeof forcedMobileValue === 'string' && !this.desktopModeEnabled) {
616620
this.forceViewportTag(viewportTag, forcedMobileValue)
617-
} else if (!viewportTag || this.desktopModeEnabled) {
621+
return
622+
}
623+
624+
// otherwise, check for special cases
625+
const forcedValues = {}
626+
627+
if (this.forcedZoomEnabled) {
628+
forcedValues['initial-scale'] = 1
629+
forcedValues['user-scalable'] = 'yes'
630+
forcedValues['maximum-scale'] = 10
631+
}
632+
633+
if (!viewportTag || this.desktopModeEnabled) {
618634
// force wide viewport width
619-
const forcedWidth = screen.width >= 1280 ? 1280 : 980
635+
forcedValues.width = screen.width >= 1280 ? 1280 : 980
636+
forcedValues['initial-scale'] = (screen.width / forcedValues.width).toFixed(3)
620637
// Race condition: depending on the loading state of the page, initial scale may or may not be respected, so the page may look zoomed-in after applying this hack.
621638
// Usually this is just an annoyance, but it may be a bigger issue if user-scalable=no is set, so we remove it too.
622-
const forcedInitialScale = (screen.width / forcedWidth).toFixed(3)
623-
let newContent = `width=${forcedWidth}, initial-scale=${forcedInitialScale}`
624-
parsedViewportContent.forEach(([key], idx) => {
625-
if (!['width', 'initial-scale', 'user-scalable'].includes(key)) {
626-
newContent = newContent.concat(`,${viewportContentParts[idx]}`) // reuse the original values, not the parsed ones
627-
}
628-
})
629-
this.forceViewportTag(viewportTag, newContent)
639+
forcedValues['user-scalable'] = 'yes'
630640
} else { // mobile mode with a viewport tag
631641
// fix an edge case where WebView forces the wide viewport
632642
const widthPart = parsedViewportContent.find(([key]) => key === 'width')
@@ -635,10 +645,24 @@ export default class WebCompat extends ContentFeature {
635645
// Chromium accepts float values for initial-scale
636646
const parsedInitialScale = parseFloat(initialScalePart[1])
637647
if (parsedInitialScale !== 1) {
638-
this.forceViewportTag(viewportTag, `width=device-width, ${viewportContent}`)
648+
forcedValues.width = 'device-width'
639649
}
640650
}
641651
}
652+
653+
const newContent = []
654+
Object.keys(forcedValues).forEach((key) => {
655+
newContent.push(`${key}=${forcedValues[key]}`)
656+
})
657+
658+
if (newContent.length > 0) { // need to override at least one viewport component
659+
parsedViewportContent.forEach(([key], idx) => {
660+
if (!(key in forcedValues)) {
661+
newContent.push(viewportContentParts[idx].trim()) // reuse the original values, not the parsed ones
662+
}
663+
})
664+
this.forceViewportTag(viewportTag, newContent.join(', '))
665+
}
642666
}
643667
}
644668

0 commit comments

Comments
 (0)