1
0
mirror of synced 2025-12-30 12:02:01 -05:00

Merge pull request #20174 from github/repo-sync

repo sync
This commit is contained in:
Octomerger Bot
2022-08-25 15:55:26 -04:00
committed by GitHub
43 changed files with 210 additions and 1008 deletions

View File

@@ -5,7 +5,7 @@ import { sendEvent, EventType } from 'components/lib/events'
import { useRouter } from 'next/router'
import { useArticleContext } from 'components/context/ArticleContext'
import parseUserAgent from 'components/lib/user-agent'
import { parseUserAgent } from 'components/lib/user-agent'
const platforms = [
{ id: 'mac', label: 'Mac' },

View File

@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import { v4 as uuidv4 } from 'uuid'
import Cookies from 'js-cookie'
import parseUserAgent from './user-agent'
import { parseUserAgent } from './user-agent'
const COOKIE_NAME = '_docs-events'

View File

@@ -19,7 +19,7 @@ const BROWSER_REGEXPS = [
/ms(ie)\/([^\s)]+)/i,
]
export default function parseUserAgent(ua = navigator.userAgent) {
export function parseUserAgent(ua = navigator.userAgent) {
ua = ua.toLowerCase()
const osRe = OS_REGEXPS.find((re) => re.test(ua))
let [, os = 'other', os_version = '0'] = (osRe && ua.match(osRe)) || []

View File

@@ -8,6 +8,7 @@ let reporters = ['default']
if (testTranslation) {
// only use custom reporter if we are linting translations
// Remove this when removing translations directory B504EDD0
reporters = ['<rootDir>/tests/helpers/lint-translation-reporter.js']
} else if (isActions) {
reporters.push('jest-github-actions-reporter')

View File

@@ -7,6 +7,8 @@
//
// [end-readme]
// Remove this when removing translations directory B504EDD0
import { execSync } from 'child_process'
import { program } from 'commander'
import fs from 'fs'

View File

@@ -1,54 +0,0 @@
import CrowdinConfig from '../helpers/crowdin-config.js'
import { loadPages } from '../../lib/page-data.js'
import { jest } from '@jest/globals'
const config = CrowdinConfig.read()
const ignoredPagePaths = config.files[0].ignore
const ignoredDataPaths = config.files[2].ignore
describe('crowdin.yml config file', () => {
jest.setTimeout(60 * 1000)
let pages
beforeAll(async () => {
pages = await loadPages()
})
test('has expected file structure', async () => {
expect(config.files.length).toBe(3)
expect(config.files[0].source).toBe('/content/**/*.md')
expect(config.files[0].ignore).toContain('/content/README.md')
})
test('ignores all Early Access paths', async () => {
expect(ignoredPagePaths).toContain('/content/early-access')
expect(ignoredDataPaths).toContain('/data/early-access')
})
test('ignores all hidden pages', async () => {
const hiddenPages = pages
.filter(
(page) => page.hidden && page.languageCode === 'en' && !page.hasExperimentalAlternative
)
.map((page) => `/content/${page.relativePath}`)
const overlooked = hiddenPages.filter((page) => !isIgnored(page, ignoredPagePaths))
const message = `Found some hidden pages that are not yet excluded from localization.
Please copy and paste the lines below into the \`ignore\` section of /crowdin.yml: \n\n"${overlooked.join(
'",\n"'
)}"`
// This may not be true anymore given the separation of Early Access docs
// expect(hiddenPages.length).toBeGreaterThan(0)
expect(ignoredPagePaths.length).toBeGreaterThan(0)
expect(overlooked, message).toHaveLength(0)
})
})
// file is ignored if its exact filename in the list,
// or if it's within an ignored directory
function isIgnored(filename, ignoredPagePaths) {
return ignoredPagePaths.some((ignoredPath) => {
const isDirectory = !ignoredPath.endsWith('.md')
return ignoredPath === filename || (isDirectory && filename.startsWith(ignoredPath))
})
}

View File

@@ -4,7 +4,6 @@ import { fileURLToPath } from 'url'
import { beforeAll, jest } from '@jest/globals'
import nock from 'nock'
import japaneseCharacters from 'japanese-characters'
import { getDOM, getJSON } from '../helpers/e2etest.js'
import enterpriseServerReleases from '../../lib/enterprise-server-releases.js'
@@ -49,23 +48,6 @@ describe('featuredLinks', () => {
).toBe(true)
})
test('localized intro links link to localized pages', async () => {
const $jaPages = await getDOM('/ja')
const $enPages = await getDOM('/en')
const $jaFeaturedLinks = $jaPages('[data-testid=article-list] a')
const $enFeaturedLinks = $enPages('[data-testid=article-list] a')
expect($jaFeaturedLinks.length).toBe($enFeaturedLinks.length)
expect($jaFeaturedLinks.eq(0).attr('href').startsWith('/ja')).toBe(true)
// Footer translations change very rarely if ever, so we can more
// reliably test those text values for the language
const footerText = []
$jaPages('footer a').each((index, element) => {
footerText.push($jaPages(element).text())
})
expect(footerText.some((elem) => japaneseCharacters.presentIn(elem)))
})
test('Enterprise user intro links have expected values', async () => {
const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/user/get-started`)
const $featuredLinks = $('[data-testid=article-list] a')

View File

@@ -37,13 +37,6 @@ describe('glossaries', () => {
})
})
test('non-English external glossary is in correct order', async () => {
const vals = loadSiteData().ja.site.data.glossaries.external
vals.forEach((val, i) => {
expect(val.term.localeCompare(vals[i + 1], 'ja')).toBeGreaterThan(0)
})
})
test('candidates all have a term, but no description', async () => {
expect(glossaries.candidates.length).toBeGreaterThan(20)
glossaries.candidates.forEach((entry) => {

View File

@@ -31,16 +31,6 @@ describe('search', () => {
})
})
})
test('has Lunr index for every language for dotcom', async () => {
expect(languageCodes.length).toBeGreaterThan(0)
languageCodes.forEach((languageCode) => {
const indexName = `${namePrefix}-dotcom-${languageCode}`
const indexRecordName = `${indexName}-records`
expect(lunrIndexNames.includes(indexName)).toBe(true)
expect(lunrIndexNames.includes(indexRecordName)).toBe(true)
})
})
})
function getDate(date) {

View File

@@ -19,7 +19,6 @@ describe('siteData module (English)', () => {
test('sets a top-level key for each language', async () => {
expect('en' in data).toEqual(true)
expect('ja' in data).toEqual(true)
})
test('includes English variables', async () => {
@@ -35,16 +34,6 @@ describe('siteData module (English)', () => {
expect(reusable).toBe('1. Change the current working directory to your local repository.')
})
test('includes Japanese variables', async () => {
const prodName = get(data, 'ja.site.data.variables.product.prodname_dotcom')
expect(prodName).toBe('GitHub')
})
test('includes Japanese reusables', async () => {
const reusable = get(data, 'ja.site.data.reusables.audit_log.octicon_icon')
expect(reusable.includes('任意のページの左上で')).toBe(true)
})
test('all Liquid tags are valid', async () => {
const dataMap = flat(data)
for (const key in dataMap) {

View File

@@ -2,7 +2,6 @@ import revalidator from 'revalidator'
import schema from '../helpers/schemas/site-tree-schema.js'
import EnterpriseServerReleases from '../../lib/enterprise-server-releases.js'
import { loadSiteTree } from '../../lib/page-data.js'
import japaneseCharacters from 'japanese-characters'
import nonEnterpriseDefaultVersion from '../../lib/non-enterprise-default-version.js'
import { jest } from '@jest/globals'
@@ -18,7 +17,6 @@ describe('siteTree', () => {
test('has language codes as top-level keys', () => {
expect('en' in siteTree).toBe(true)
expect('ja' in siteTree).toBe(true)
})
test('object order and structure', () => {
@@ -29,19 +27,6 @@ describe('siteTree', () => {
})
describe('localized titles', () => {
// skipped because it has rendering errors. See translations/log/ja-resets.csv
test.skip('titles for categories', () => {
const japaneseTitle =
siteTree.ja[nonEnterpriseDefaultVersion].childPages[0].childPages[0].page.title
expect(typeof japaneseTitle).toBe('string')
expect(japaneseCharacters.presentIn(japaneseTitle)).toBe(true)
const englishTitle =
siteTree.en[nonEnterpriseDefaultVersion].childPages[0].childPages[0].page.title
expect(typeof englishTitle).toBe('string')
expect(japaneseCharacters.presentIn(englishTitle)).toBe(false)
})
test('articles that include site data in liquid templating', async () => {
const ghesLatest = `enterprise-server@${latestEnterpriseRelease}`
const ghesSiteTree = siteTree.en[ghesLatest]

View File

@@ -1,12 +0,0 @@
import { fileURLToPath } from 'url'
import path from 'path'
import fs from 'fs'
import yaml from 'js-yaml'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export const read = function () {
const filename = path.join(__dirname, '../../crowdin.yml')
return yaml.load(fs.readFileSync(filename, 'utf8'), { filename })
}
export default { read }

View File

@@ -10,7 +10,6 @@ export async function get(
followRedirects: false,
followAllRedirects: false,
headers: {},
cookieJar: undefined,
}
) {
const method = opts.method || 'get'
@@ -22,7 +21,6 @@ export async function get(
body: opts.body,
headers: opts.headers,
retry: { limit: 0 },
cookieJar: opts.cookieJar,
throwHttpErrors: false,
followRedirect: opts.followAllRedirects || opts.followRedirects,
},
@@ -62,14 +60,13 @@ export function post(route, opts) {
export async function getDOM(
route,
{ headers, allow500s, allow404, cookieJar } = {
{ headers, allow500s, allow404 } = {
headers: undefined,
allow500s: false,
allow404: false,
cookieJar: undefined,
}
) {
const res = await get(route, { followRedirects: true, headers, cookieJar })
const res = await get(route, { followRedirects: true, headers })
if (!allow500s && res.status >= 500) {
throw new Error(`Server error (${res.status}) on ${route}`)
}

View File

@@ -1,3 +1,5 @@
// Remove this when removing translations directory B504EDD0
import chalk from 'chalk'
import stripAnsi from 'strip-ansi'
import { groupBy } from 'lodash-es'

View File

@@ -1,48 +0,0 @@
export default {
properties: {
name: {
required: true,
description: 'the English name',
type: 'string',
},
nativeName: {
description: 'the native name',
type: 'string',
},
code: {
required: true,
description: 'the code used in the URL',
type: 'string',
minLength: 2,
maxLength: 2,
},
dir: {
required: true,
description: 'the local relative path to files in this language',
type: 'string',
},
// https://support.google.com/webmasters/answer/189077
// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
// http://unicode.org/iso15924/iso15924-codes.html
hreflang: {
required: true,
description: 'the ISO 639-1, ISO 3166-1 Alpha 2, or ISO 15924 language code',
type: 'string',
minLength: 2,
},
redirectPatterns: {
description: 'array of regular expressions used for redirecting incorrect URLs',
type: 'array',
},
wip: {
description: 'boolean indicating whether translations are incomplete',
type: 'boolean',
},
},
}

View File

@@ -277,6 +277,7 @@ if (!process.env.TEST_TRANSLATION) {
)
learningTracksToLint = zip(learningTracksYamlRelPaths, learningTracksYamlAbsPaths)
} else {
// Remove this `else` when removing translations directory B504EDD0
// get all translated markdown or yaml files by comparing files changed to main branch
const changedFilesRelPaths = execSync(
'git -c diff.renameLimit=10000 diff --name-only origin/main',

View File

@@ -1,5 +1,4 @@
import { blockIndex } from '../../middleware/block-robots.js'
import languages from '../../lib/languages.js'
import { productMap } from '../../lib/all-products.js'
import enterpriseServerReleases from '../../lib/enterprise-server-releases.js'
@@ -14,13 +13,6 @@ describe('block robots', () => {
expect(allowIndex('/en/articles/verifying-your-email-address')).toBe(true)
})
it('allows crawling of generally available localized content', async () => {
Object.values(languages).forEach((language) => {
expect(allowIndex(`/${language.code}`)).toBe(true)
expect(allowIndex(`/${language.code}/articles/verifying-your-email-address`)).toBe(true)
})
})
it('disallows crawling of WIP products', async () => {
const wipProductIds = Object.values(productMap)
.filter((product) => product.wip)
@@ -29,19 +21,11 @@ describe('block robots', () => {
wipProductIds.forEach((id) => {
const { href } = productMap[id]
const blockedPaths = [
// English
`/en${href}`,
`/en${href}/overview`,
`/en${href}/overview/intro`,
`/en/enterprise/${enterpriseServerReleases.latest}/user${href}`,
`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}`,
// Japanese
`/ja${href}`,
`/ja${href}/overview`,
`/ja${href}/overview/intro`,
`/ja/enterprise/${enterpriseServerReleases.latest}/user${href}`,
`/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}`,
]
blockedPaths.forEach((path) => {
@@ -59,14 +43,7 @@ describe('block robots', () => {
const { versions } = productMap[id]
const blockedPaths = versions
.map((version) => {
return [
// English
`/en/${version}/${id}`,
`/en/${version}/${id}/some-early-access-article`,
// Japanese
`/ja/${version}/${id}`,
`/ja/${version}/${id}/some-early-access-article`,
]
return [`/en/${version}/${id}`, `/en/${version}/${id}/some-early-access-article`]
})
.flat()
@@ -90,16 +67,10 @@ describe('block robots', () => {
it('disallows crawling of deprecated enterprise releases', async () => {
enterpriseServerReleases.deprecated.forEach((version) => {
const blockedPaths = [
// English
`/en/enterprise-server@${version}/actions`,
`/en/enterprise/${version}/actions`,
`/en/enterprise-server@${version}/actions/overview`,
`/en/enterprise/${version}/actions/overview`,
// Japanese
`/ja/enterprise-server@${version}/actions`,
`/ja/enterprise/${version}/actions`,
`/ja/enterprise-server@${version}/actions/overview`,
`/ja/enterprise/${version}/actions/overview`,
]
blockedPaths.forEach((path) => {

View File

@@ -83,12 +83,6 @@ describe('breadcrumbs', () => {
const $breadcrumbs = $('[data-testid=breadcrumbs] a')
expect($breadcrumbs[0].attribs.href).toBe('/en/get-started')
})
test('localized breadcrumbs link to localize pages', async () => {
const $ = await getDOM('/ja/get-started/learning-about-github')
const $breadcrumbs = $('[data-testid=breadcrumbs] a')
expect($breadcrumbs[0].attribs.href).toBe('/ja/get-started')
})
})
describeInternalOnly('early access rendering', () => {

View File

@@ -29,25 +29,4 @@ describe('curated homepage links', () => {
expect($(el).find('p p').length).toBe(0)
})
})
test('Japanese', async () => {
const $ = await getDOM('/ja')
const $links = $('[data-testid=bump-link]')
expect($links.length).toBeGreaterThanOrEqual(8)
// Check that each link is localized and includes a title and intro
$links.each((i, el) => {
const linkUrl = $(el).attr('href')
expect(linkUrl.startsWith('/ja/')).toBe(true)
expect(
$(el).find('[data-testid=link-with-intro-title]').text().trim().length,
`Did not find a title for the linked article ${linkUrl}`
).toBeGreaterThan(0)
expect(
$(el).find('[data-testid=link-with-intro-intro]').text().trim().length,
`Did not find an intro for the linked article ${linkUrl}`
).toBeGreaterThan(0)
})
})
})

View File

@@ -1,5 +1,4 @@
import { getDOM } from '../helpers/e2etest.js'
import languages from '../../lib/languages.js'
import { jest } from '@jest/globals'
jest.useFakeTimers({ legacyFakeTimers: true })
@@ -7,24 +6,6 @@ jest.useFakeTimers({ legacyFakeTimers: true })
describe('<head>', () => {
jest.setTimeout(5 * 60 * 1000)
test('includes hreflangs (references to all language versions of the same page)', async () => {
const $ = await getDOM('/en')
const $hreflangs = $('link[rel="alternate"]')
expect($hreflangs.length).toEqual(Object.keys(languages).length)
expect($('link[href="https://docs.github.com/cn"]').length).toBe(1)
expect($('link[href="https://docs.github.com/ja"]').length).toBe(1)
// Due to a bug in either NextJS, JSX, or TypeScript,
// when put `<link hrefLang="xxx">` in a .tsx file, this incorrectly
// gets rendered out as `<link hrefLang="xxx">` in the final HTML.
// Note the uppercase L. It's supposed to become `<link hreflang="xxx">`.
// When cheerio serializes to HTML, it gets this right so it lowercases
// the attribute. So if this rendering in this jest test was the first
// ever cold hit, you might get the buggy HTML from React or you
// might get the correct HTML from cheerio's `.html()` serializer.
// This is why we're looking for either.
expect($('link[hreflang="en"]').length + $('link[hrefLang="en"]').length).toBe(1)
})
test('includes page intro in `description` meta tag', async () => {
const $ = await getDOM('/en/articles/about-ssh')
const $description = $('meta[name="description"]')

View File

@@ -6,99 +6,6 @@ import { oldestSupported } from '../../lib/enterprise-server-releases.js'
describe('header', () => {
jest.setTimeout(5 * 60 * 1000)
test('includes localized meta tags', async () => {
const $ = await getDOM('/en')
expect($('link[rel="alternate"]').length).toBeGreaterThan(2)
})
test("includes a link to the homepage (in the current page's language)", async () => {
let $ = await getDOM('/en')
expect($('#github-logo a[href="/en"]').length).toBe(2)
$ = await getDOM('/ja')
expect($('#github-logo a[href="/ja"]').length).toBe(2)
expect($('#github-logo a[href="/en"]').length).toBe(0)
})
describe.skip('language links', () => {
test('lead to the same page in a different language', async () => {
const $ = await getDOM(
'/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule'
)
expect(
$(
'li a[href="/ja/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule"]'
).length
).toBe(1)
})
test('display the native name and the English name for each translated language', async () => {
const $ = await getDOM('/en')
expect($('[data-testid=language-picker] li a[href="/en"]').text().trim()).toBe('English')
expect($('[data-testid=language-picker] li a[href="/cn"]').text().trim()).toBe('简体中文')
expect($('[data-testid=language-picker] li a[href="/ja"]').text().trim()).toBe('日本語')
})
test('emphasize the current language', async () => {
const $ = await getDOM('/en')
expect($('[data-testid=desktop-header] [data-testid=language-picker] button').text()).toBe(
'English'
)
})
})
describe('notices', () => {
// Docs engineering issue: 1055
test.skip('displays a "localization in progress" notice for WIP languages', async () => {
const $ = await getDOM('/de')
expect($('[data-testid=header-notification][data-type=TRANSLATION]').length).toBe(1)
expect($('[data-testid=header-notification] a[href="/en"]').length).toBe(1)
})
test('displays "complete" notice for non-WIP non-English languages', async () => {
const $ = await getDOM('/ja')
expect($('[data-testid=header-notification][data-type=TRANSLATION]').length).toBe(1)
expect($('[data-testid=header-notification] a[href="/en"]').length).toBe(1)
})
// Docs Engineering issue: 966
test.skip('does not display any notices for English', async () => {
const $ = await getDOM('/en')
expect($('[data-testid=header-notification]').length).toBe(0)
})
test.skip("renders a link to the same page in user's preferred language, if available", async () => {
// const headers = { 'accept-language': 'ja' }
// const $ = await getDOM('/en', { headers })
// expect(getUserLanguage($)).toBe('ja')
})
test.skip("renders a link to the same page if user's preferred language is Chinese - PRC", async () => {
// const headers = { 'accept-language': 'zh-CN' }
// const $ = await getDOM('/en', { headers })
// expect(getUserLanguage($)).toBe('cn')
})
test.skip("renders a link to the same page in user's preferred language from multiple, if available", async () => {
// const headers = { 'accept-language': 'ja, *;q=0.9' }
// const $ = await getDOM('/en', { headers })
// expect(getUserLanguage($)).toBe('ja')
})
test.skip("renders a link to the same page in user's preferred language with weights, if available", async () => {
// const headers = { 'accept-language': 'ja;q=1.0, *;q=0.9' }
// const $ = await getDOM('/en', { headers })
// expect(getUserLanguage($)).toBe('ja')
})
test.skip("renders a link to the user's 2nd preferred language if 1st is not available", async () => {
// const headers = { 'accept-language': 'zh-TW,zh;q=0.9,ja *;q=0.8' }
// const $ = await getDOM('/en', { headers })
// expect(getUserLanguage($)).toBe('ja')
})
})
describe('mobile-only product dropdown links', () => {
test('include Get started and admin, and emphasize the current product', async () => {
const $ = await getDOM(
@@ -113,14 +20,6 @@ describe('header', () => {
expect(ghec.text().trim()).toBe('Enterprise administrators')
})
test("point to homepages in the current page's language", async () => {
const $ = await getDOM('/ja/github/site-policy/github-terms-of-service')
const $breadcrumbRefs = $('[data-testid=breadcrumbs] a')
expect($breadcrumbRefs[0].attribs.href.startsWith('/ja')).toBe(true)
const $sidebarRefs = $('[data-testid=sidebar] a')
expect($sidebarRefs[0].attribs.href.startsWith('/ja')).toBe(true)
})
test('emphasizes the product that corresponds to the current page', async () => {
const $ = await getDOM(
`/en/enterprise-server@${oldestSupported}/get-started/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line`

View File

@@ -51,13 +51,4 @@ describe('page titles', () => {
`GitHub Enterprise Server Help Documentation - GitHub Enterprise Server ${enterpriseServerReleases.latest} Docs`
)
})
// TODO enable this once translated content has synced with the versioning changes
// Note the expected translations may need to be updated, since the English title changed
// from `GitHub.com Help Documentation` to `GitHub Documentation`
// Docs Engineering issue: 967
test.skip('displays only the site name on localized homepages', async () => {
expect((await getDOM('/cn'))('title').text()).toBe('GitHub 帮助文档')
expect((await getDOM('/ja'))('title').text()).toBe('GitHub ヘルプドキュメント')
})
})

View File

@@ -16,9 +16,6 @@ describe('mobile-only products nav', () => {
['/desktop', 'GitHub Desktop'],
['/actions', 'GitHub Actions'],
// localized
['/ja/desktop', 'GitHub Desktop'],
]
test.each(cases)('on %p, renders current product %p', async (url, name) => {

View File

@@ -1,4 +1,3 @@
import languages from '../../lib/languages.js'
import robotsParser from 'robots-parser'
import { get } from '../helpers/e2etest.js'
import { jest } from '@jest/globals'
@@ -24,17 +23,6 @@ describe('robots.txt', () => {
).toBe(true)
})
it('allows indexing of generally available localized content', async () => {
Object.values(languages).forEach((language) => {
expect(robots.isAllowed(`https://docs.github.com/${language.code}`)).toBe(true)
expect(
robots.isAllowed(
`https://docs.github.com/${language.code}/articles/verifying-your-email-address`
)
).toBe(true)
})
})
it('disallows indexing of azurecontainer.io domains', async () => {
const res = await get('/robots.txt', {
headers: {

View File

@@ -7,7 +7,6 @@ import CspParse from 'csp-parse'
import { productMap } from '../../lib/all-products.js'
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
import { describe, jest } from '@jest/globals'
import { languageKeys } from '../../lib/languages.js'
const AZURE_STORAGE_URL = 'githubdocs.azureedge.net'
const activeProducts = Object.values(productMap).filter(
@@ -141,12 +140,6 @@ describe('server', () => {
expect($('body').length).toBe(1)
})
test('sets `lang` attribute on <html> attribute', async () => {
expect((await getDOM('/en'))('html').attr('lang')).toBe('en')
expect((await getDOM('/en/articles/set-up-git'))('html').attr('lang')).toBe('en')
expect((await getDOM('/ja'))('html').attr('lang')).toBe('ja')
})
test('renders a 404 page', async () => {
const $ = await getDOM('/not-a-real-page', { allow404: true })
expect($('h1').text()).toBe('Ooops!')
@@ -360,12 +353,6 @@ describe('server', () => {
const $ = await getDOM('/en/actions/using-workflows/workflow-syntax-for-github-actions')
expect($('h2#in-this-article + nav ul li a[href="#on"]').length).toBe(1)
})
// TODO
test('renders mini TOC with correct links when headings contain markup in localized content', async () => {
const $ = await getDOM('/ja/actions/using-workflows/workflow-syntax-for-github-actions')
expect($('h2#in-this-article + nav ul li a[href="#on"]').length).toBe(1)
})
})
describe('image asset paths', () => {
@@ -633,23 +620,6 @@ describe('server', () => {
expect(res.headers['set-cookie']).toBeUndefined()
})
test('redirects / to appropriate language preference if specified', async () => {
await Promise.all(
languageKeys.map(async (languageKey) => {
const res = await get('/', {
headers: {
'accept-language': `${languageKey}`,
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe(`/${languageKey}`)
expect(res.headers['cache-control']).toBe('private, no-store')
expect(res.headers['set-cookie']).toBeUndefined()
})
)
})
// This test exists because in a previous life, our NextJS used to
// 500 if the 'Accept-Language' header was malformed.
// We *used* have a custom middleware to cope with this and force a
@@ -749,18 +719,6 @@ describe('server', () => {
})
})
describe('URLs by language', () => {
test('heading IDs and links on translated pages are in English', async () => {
const $ = await getDOM('/ja/site-policy/github-terms/github-terms-of-service')
expect($.res.statusCode).toBe(200)
// This check is true on either the translated version of the page, or when the title is pending translation and is in English.
expect($('h1')[0].children[0].data).toMatch(
/(GitHub利用規約|GitHub Terms of Service|GitHub のサービス条件)/
)
expect($('h2 a[href="#summary"]').length).toBe(1)
})
})
describe('GitHub Enterprise URLs', () => {
test('renders the GHE user docs homepage', async () => {
const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/user/get-started`)
@@ -798,12 +756,6 @@ describe('GitHub Enterprise URLs', () => {
expect($(`a[href^="${installationCategoryHome}/"]`).length).toBeGreaterThan(1)
})
test('renders a translated Enterprise Admin category with English links', async () => {
const installationCategoryHome = `/ja/enterprise-server@${enterpriseServerReleases.latest}/admin/installation`
const $ = await getDOM(installationCategoryHome)
expect($(`a[href^="${installationCategoryHome}/"]`).length).toBeGreaterThan(1)
})
test('renders an Enterprise Admin category article', async () => {
const $ = await getDOM(
`/en/enterprise/${enterpriseServerReleases.latest}/admin/overview/about-github-enterprise-server`
@@ -828,26 +780,6 @@ describe('GitHub Enterprise URLs', () => {
)
expect($.text()).toContain('Before upgrading GitHub Enterprise')
})
test('renders Enterprise homepage in Japanese', async () => {
const res = await get(`/ja/enterprise-server@${enterpriseServerReleases.latest}`)
expect(res.statusCode).toBe(200)
})
test('renders Enterprise Admin homepage in Japanese', async () => {
const res = await get(`/ja/enterprise-server@${enterpriseServerReleases.latest}/admin`)
expect(res.statusCode).toBe(200)
})
test('renders Enterprise homepage in Chinese', async () => {
const res = await get(`/cn/enterprise-server@${enterpriseServerReleases.latest}`)
expect(res.statusCode).toBe(200)
})
test('renders Enterprise Admin homepage in Chinese', async () => {
const res = await get(`/cn/enterprise-server@${enterpriseServerReleases.latest}/admin`)
expect(res.statusCode).toBe(200)
})
})
describe('GitHub Desktop URLs', () => {
@@ -878,11 +810,6 @@ describe('GitHub Desktop URLs', () => {
)
expect(res.statusCode).toBe(200)
})
test('renders the Desktop homepage in Japanese', async () => {
const res = await get('/ja/desktop')
expect(res.statusCode).toBe(200)
})
})
describe('extended Markdown', () => {
@@ -1056,15 +983,8 @@ describe('REST reference pages', () => {
const res = await get('/en/rest/repos')
expect(res.statusCode).toBe(200)
})
test('view the rest/repos page in Japanese', async () => {
const res = await get('/ja/rest/repos')
expect(res.statusCode).toBe(200)
})
test('deeper pages in English', async () => {
const res = await get('/ja/enterprise-cloud@latest/rest/code-scanning')
expect(res.statusCode).toBe(200)
})
test('deeper pages in Japanese', async () => {
const res = await get('/en/enterprise-cloud@latest/rest/code-scanning')
expect(res.statusCode).toBe(200)
})

View File

@@ -3,7 +3,6 @@ import { describe, jest, test } from '@jest/globals'
import enterpriseServerReleases from '../../lib/enterprise-server-releases.js'
import { get, getDOM } from '../helpers/e2etest.js'
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
import { PREFERRED_LOCALE_COOKIE_NAME } from '../../lib/constants.js'
jest.useFakeTimers({ legacyFakeTimers: true })
@@ -99,16 +98,7 @@ describe('recently deprecated redirects', () => {
// Deliberately no cache control because it is user-dependent
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('basic enterprise 3.0 redirects by cookie', async () => {
const res = await get('/enterprise/3.0', {
headers: {
Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=ja`,
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/ja/enterprise-server@3.0')
})
test('already languaged enterprise 3.0 redirects', async () => {
const res = await get('/en/enterprise/3.0')
expect(res.statusCode).toBe(301)
@@ -132,20 +122,7 @@ describe('recently deprecated redirects', () => {
'/en/enterprise-server@3.0/get-started/learning-about-github/githubs-products'
)
})
test('redirects enterprise-server 3.0 with actual redirect with language', async () => {
const res = await get(
'/ja/enterprise-server@3.0/github/getting-started-with-github/githubs-products'
)
expect(res.statusCode).toBe(301)
expect(res.headers['set-cookie']).toBeUndefined()
expect(res.headers['cache-control']).toContain('public')
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
// This is based on
// https://github.com/github/help-docs-archived-enterprise-versions/blob/master/3.0/redirects.json
expect(res.headers.location).toBe(
'/ja/enterprise-server@3.0/get-started/learning-about-github/githubs-products'
)
})
test('follow redirects enterprise-server 3.0 with actual redirect without language', async () => {
const res = await get(
'/enterprise-server@3.0/github/getting-started-with-github/githubs-products',

View File

@@ -1,27 +0,0 @@
import { jest } from '@jest/globals'
import { get } from '../helpers/e2etest.js'
describe('language code redirects', () => {
jest.setTimeout(5 * 60 * 1000)
test('redirects accidental /jp* requests to /ja*', async () => {
let res = await get('/jp')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('/ja')
res = await get('/jp/articles/about-your-personal-dashboard')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('/ja/articles/about-your-personal-dashboard')
})
test('redirects accidental /zh-CN* requests to /cn*', async () => {
let res = await get('/zh-CN')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('/cn')
res = await get('/zh-TW/articles/about-your-personal-dashboard')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('/cn/articles/about-your-personal-dashboard')
})
})

View File

@@ -9,7 +9,6 @@ import enterpriseServerReleases, {
import Page from '../../lib/page.js'
import { get, head } from '../helpers/e2etest.js'
import versionSatisfiesRange from '../../lib/version-satisfies-range.js'
import { PREFERRED_LOCALE_COOKIE_NAME } from '../../lib/constants.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -128,39 +127,6 @@ describe('redirects', () => {
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('homepage redirects to preferred language', async () => {
const res = await get('/', { headers: { 'Accept-Language': 'ja' }, followRedirects: false })
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/ja')
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('homepage redirects to preferred language by cookie', async () => {
const res = await get('/', {
headers: {
Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=ja`,
'Accept-Language': 'es', // note how this is going to be ignored
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/ja')
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('homepage redirects to preferred language by cookie if valid', async () => {
const res = await get('/', {
headers: {
Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=xy`,
'Accept-Language': 'ja', // note how this is going to be ignored
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/ja')
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('trailing slash on languaged homepage should permantently redirect', async () => {
const res = await get('/en/')
expect(res.statusCode).toBe(301)
@@ -169,15 +135,6 @@ describe('redirects', () => {
expect(res.headers['cache-control']).toContain('public')
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
})
test('trailing slash with query string on languaged homepage should permantently redirect', async () => {
const res = await get('/ja/?foo=bar&bar=foo')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('/ja?foo=bar&bar=foo')
expect(res.headers['set-cookie']).toBeUndefined()
expect(res.headers['cache-control']).toContain('public')
expect(res.headers['cache-control']).toMatch(/max-age=\d+/)
})
})
describe('external redirects', () => {
@@ -210,87 +167,10 @@ describe('redirects', () => {
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('https://gitready.com/')
})
test('work for top-level request paths with /ja/ prefix', async () => {
const res = await get('/ja/git-ready')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('https://gitready.com/')
})
})
describe('localized redirects', () => {
const redirectFrom =
'/desktop/contributing-to-projects/changing-a-remote-s-url-from-github-desktop'
const redirectTo =
'/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/changing-a-remotes-url-from-github-desktop'
test('redirect_from for renamed pages', async () => {
const res = await get(`/ja${redirectFrom}`)
expect(res.statusCode).toBe(301)
const expected = `/ja${redirectTo}`
expect(res.headers.location).toBe(expected)
})
test('redirect_from for renamed pages by Accept-Language header', async () => {
const res = await get(redirectFrom, {
headers: {
'Accept-Language': 'ja',
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
const expected = `/ja${redirectTo}`
expect(res.headers.location).toBe(expected)
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('redirect_from for renamed pages but ignore Accept-Language header if not recognized', async () => {
const res = await get(redirectFrom, {
headers: {
// None of these are recognized
'Accept-Language': 'sv,fr,gr',
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
const expected = `/en${redirectTo}`
expect(res.headers.location).toBe(expected)
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('redirect_from for renamed pages but ignore unrecognized Accept-Language header values', async () => {
const res = await get(redirectFrom, {
headers: {
// Only the last one is recognized
'Accept-Language': 'sv,ja',
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
const expected = `/ja${redirectTo}`
expect(res.headers.location).toBe(expected)
expect(res.headers['cache-control']).toBe('private, no-store')
})
test('will inject the preferred language from cookie', async () => {
const res = await get(redirectFrom, {
headers: {
Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=ja`,
'Accept-Language': 'es', // note how this is going to be ignored
},
followRedirects: false,
})
// 302 because the redirect depended on cookie
expect(res.statusCode).toBe(302)
const expected = `/ja${redirectTo}`
expect(res.headers.location).toBe(expected)
expect(res.headers['cache-control']).toBe('private, no-store')
})
})
describe('enterprise home page', () => {
const enterpriseHome = `/en/enterprise-server@${enterpriseServerReleases.latest}`
const japaneseEnterpriseHome = enterpriseHome.replace('/en/', '/ja/')
test('/enterprise', async () => {
const res = await get('/enterprise')
@@ -310,12 +190,6 @@ describe('redirects', () => {
expect(res.headers.location).toBe(enterpriseHome)
})
test('no version redirects to latest version (japanese)', async () => {
const res = await get('/ja/enterprise')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(japaneseEnterpriseHome)
})
test('hardcoded @latest redirects to latest version', async () => {
const res = await get('/en/enterprise-server@latest')
expect(res.statusCode).toBe(302)
@@ -358,7 +232,6 @@ describe('redirects', () => {
const { firstRestoredAdminGuides, getPreviousReleaseNumber, latest } = enterpriseServerReleases
const lastBeforeRestoredAdminGuides = getPreviousReleaseNumber(firstRestoredAdminGuides)
const enterpriseAdmin = `/en/enterprise-server@${latest}/admin`
const japaneseEnterpriseAdmin = enterpriseAdmin.replace('/en/', '/ja/')
test('no language code redirects to english', async () => {
const res = await get(`/enterprise/${latest}/admin`)
@@ -413,30 +286,10 @@ describe('redirects', () => {
const expected = `/en/enterprise-server@${firstRestoredAdminGuides}/admin/enterprise-management/updating-the-virtual-machine-and-physical-resources/upgrading-github-enterprise-server`
expect(res.headers.location).toBe(expected)
})
test('no version redirects to latest version (japanese)', async () => {
const res = await get('/ja/enterprise/admin')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(japaneseEnterpriseAdmin)
})
test('admin/guides redirects to admin on <2.21 (japanese)', async () => {
const res = await get(`/ja/enterprise-server@${lastBeforeRestoredAdminGuides}/admin/guides`)
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(
japaneseEnterpriseAdmin.replace(latest, lastBeforeRestoredAdminGuides)
)
})
test('admin/guides does not redirect to admin on >=2.21 (japanese)', async () => {
const res = await get(`/ja/enterprise-server@${firstRestoredAdminGuides}/admin/guides`)
expect(res.statusCode).toBe(200)
})
})
describe('enterprise user homepage', () => {
const enterpriseUser = `/en/enterprise-server@${enterpriseServerReleases.latest}`
const japaneseEnterpriseUser = enterpriseUser.replace('/en/', '/ja/')
test('no product redirects to GitHub.com product', async () => {
const res = await get(`/en/enterprise/${enterpriseServerReleases.latest}/user`)
@@ -455,17 +308,10 @@ describe('redirects', () => {
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(enterpriseUser)
})
test('no version redirects to latest version (japanese)', async () => {
const res = await get('/ja/enterprise/user/github')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(japaneseEnterpriseUser)
})
})
describe('enterprise user article', () => {
const userArticle = `/en/enterprise-server@${enterpriseServerReleases.latest}/get-started/quickstart/fork-a-repo`
const japaneseUserArticle = userArticle.replace('/en/', '/ja/')
test('no product redirects to GitHub.com product on the latest version', async () => {
const res = await get(
@@ -494,18 +340,11 @@ describe('redirects', () => {
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(userArticle)
})
test('no version redirects to latest version (japanese)', async () => {
const res = await get('/ja/enterprise/articles/fork-a-repo')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(japaneseUserArticle)
})
})
describe('enterprise user article with frontmatter redirect', () => {
const userArticle = `/en/enterprise-server@${enterpriseServerReleases.latest}/get-started/quickstart/fork-a-repo`
const redirectFromPath = '/articles/fork-a-repo'
const japaneseUserArticle = userArticle.replace('/en/', '/ja/')
test('redirects to expected article', async () => {
const res = await get(
@@ -528,18 +367,11 @@ describe('redirects', () => {
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(userArticle)
})
test('no version redirects to latest version (japanese)', async () => {
const res = await get(`/ja/enterprise/user${redirectFromPath}`)
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(japaneseUserArticle)
})
})
describe('desktop guide', () => {
const desktopGuide =
'/en/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/creating-an-issue-or-pull-request'
const japaneseDesktopGuides = desktopGuide.replace('/en/', '/ja/')
test('no language code redirects to english', async () => {
const res = await get(
@@ -556,14 +388,6 @@ describe('redirects', () => {
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(desktopGuide)
})
test('desktop/guides redirects to desktop (japanese)', async () => {
const res = await get(
'/ja/desktop/guides/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request'
)
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe(japaneseDesktopGuides)
})
})
describe('recently deprecated ghes version redirects that lack language', () => {

View File

@@ -1,13 +0,0 @@
import {execSync} from 'child_process'
describe('source directories', () => {
// crowdin upload sources command fails if there are empty source files
// please refer to crowdin-support #117 for more details
it('should not contain empty files', () => {
const command = "find content data -type f -empty"
const emptyFiles = execSync(command).toString().split("\n")
const disallowedEmptyFiles = emptyFiles.filter(file => file.match(/\.(yml|md)$/))
expect(disallowedEmptyFiles).toEqual([])
})
})

View File

@@ -0,0 +1,72 @@
import languages from '../../lib/languages.js'
import { omit } from 'lodash-es'
import walk from 'walk-sync'
import { execSync } from 'child_process'
import { get } from '../helpers/e2etest.js'
import { jest } from '@jest/globals'
function difference (A, B) {
const A2 = new Set(A)
B.forEach(b => A2.delete(b))
return A2
}
const englishPaths = new Set(walk('content', {
directories: false,
ignore: ['**/README.md', 'search', 'early-access'],
}))
const nonEnglish = Object.entries(omit(languages, 'en'))
const langWalksTable = nonEnglish.map(([code, lang]) => [
code,
lang,
new Set(walk(`${lang.dir}/content`, {
directories: false,
ignore: ['**/README.md'],
}))
])
describe('files', () => {
jest.setTimeout(60 * 1000)
test.each(Object.entries(languages))('%s matches language schema', async (_, lang) => {
expect(lang.name).toMatch(/\w+/)
if (lang.nativeName) expect(lang.nativeName).toMatch(/.+/)
expect(lang.code).toMatch(/\w{2}/)
if (lang.code !== 'en') expect(lang.dir).toMatch(/.+/)
expect(lang.hreflang).toMatch(/\w{2}/)
if (lang.redirectPatterns) expect(lang.redirectPatterns).toBeInstanceOf(Array)
})
// crowdin upload sources command fails if there are empty source files
// please refer to crowdin-support #117 for more details
it('should not contain empty files', () => {
const command = "find content data -type f -empty"
const emptyFiles = execSync(command).toString().split("\n")
const disallowedEmptyFiles = emptyFiles.filter(file => file.match(/\.(yml|md)$/))
expect(disallowedEmptyFiles).toEqual([])
})
test.each(langWalksTable)("falls back to the English page if it can't find a %s page", async (code, lang, langPaths) => {
const paths = [...difference(englishPaths, langPaths)]
.map(path => path.replace('/index.md', ''))
.map(path => path.replace('.md', ''))
for (const path of paths) {
const res = await get(`/${code}/${path}`)
expect(res.statusCode, path).toBe(200)
}
})
test.each(langWalksTable)("only loads a %s page if there's an English page", async (code, lang, langPaths) => {
const paths = [...difference(langPaths, englishPaths)]
.map(path => path.replace('/index.md', ''))
.map(path => path.replace('.md', ''))
for (const path of paths) {
const res = await get(`/${code}/${path}`)
expect(res.statusCode, path).toBeGreaterThanOrEqual(300)
expect(res.statusCode, path).toBeLessThanOrEqual(499)
}
})
})

View File

@@ -0,0 +1,62 @@
import { languageKeys } from '../../lib/languages.js'
import { blockIndex } from '../../middleware/block-robots.js'
import { getDOM } from '../helpers/e2etest.js'
const langs = languageKeys.filter(lang => lang !== 'en')
describe('frame', () => {
test.each(langs)('allows crawling of %s pages', async (lang) => {
expect(blockIndex(`/${lang}/articles/verifying-your-email-address`)).toBe(false)
})
test.each(langs)('breadcrumbs link to %s pages', async (lang) => {
const $ = await getDOM(`/${lang}/get-started/learning-about-github`)
const $breadcrumbs = $('[data-testid=breadcrumbs] a')
expect($breadcrumbs[0].attribs.href).toBe(`/${lang}/get-started`)
})
test.each(langs)('homepage links go to %s pages', async (lang) => {
const $ = await getDOM(`/${lang}`)
const $links = $('[data-testid=bump-link]')
$links.each((i, el) => {
const linkUrl = $(el).attr('href')
expect(linkUrl.startsWith(`/${lang}/`)).toBe(true)
})
})
test.each(langs)('includes homepage hreflang to %s', async (lang) => {
const $ = await getDOM('/en')
expect($(`link[rel="alternate"][href="https://docs.github.com/${lang}"]`).length).toBe(1)
})
test.each(langs)('sets `lang` attribute on <html> attribute in %s', async (lang) => {
const $ = await getDOM(`/${lang}`)
expect($('html').attr('lang')).toBe(lang)
})
test.each(langs)('autogenerated heading IDs on %s are in english', async (lang) => {
const $ = await getDOM(`/${lang}/site-policy/github-terms/github-terms-of-service`)
expect($('h2 a[href="#summary"]').length).toBe(1)
})
test.each(langs)('loads the side bar via site tree in %s', async (lang) => {
const $en = await getDOM(`/en/get-started`)
const $ = await getDOM(`/${lang}/get-started`)
expect(
$(`a[href="/${lang}/get-started"]`).text()
).not.toEqual(
$en(`a[href="/${lang}/get-started"]`).text()
)
})
test.each(langs)('loads the survey via site data in %s', async (lang) => {
const $en = await getDOM(`/en`)
const $ = await getDOM(`/${lang}`)
expect(
$('[data-testid="survey-form"] h2').text()
).not.toEqual(
$en('[data-testid="survey-form"] h2').text()
)
})
})

View File

@@ -1,65 +0,0 @@
import languages from '../../lib/languages.js'
import { languageFiles, compareLiquidTags } from '../../lib/liquid-tags/tokens.js'
const currentTranslations = []
// populate currentTranslations
Object.keys(languages).forEach((code) => {
const language = languages[code]
if (!language.wip && code !== 'en') {
currentTranslations.push(language)
}
})
function diffToErrorString(diff) {
return `
${diff.translation} does not match liquid tags in ${diff.file}:
${diff.diff.output}
`
}
function formatMessage(parsingErrors, mismatches) {
return `
${parsingErrors.length} files have parsing errors.
${mismatches.length} files have mismatching liquid tags.
${'#'.repeat(80)}
Parsing errors were found in the following files:
${'#'.repeat(80)}
${parsingErrors.map((error) => `- ${error.message}`).join('\n')}
${'#'.repeat(80)}
${mismatches.length} files do not match liquid tags:
${'#'.repeat(80)}
${mismatches.map((diff) => diffToErrorString(diff)).join('\n')}
`
}
describe('translated pages', () => {
currentTranslations.forEach((language) => {
test.skip(`every ${language.name} has the same number of liquid tags as the English one`, () => {
const mismatches = []
const files = languageFiles(language)
const parsingErrors = []
files.forEach((file) => {
try {
const comparison = compareLiquidTags(file, language)
if (comparison.diff.count > 0) {
mismatches.push(comparison)
}
} catch (e) {
parsingErrors.push(e)
}
})
const message = formatMessage(parsingErrors, mismatches)
expect(mismatches.length, message).toEqual(0)
})
})
})

View File

@@ -0,0 +1,44 @@
import { languageKeys } from '../../lib/languages.js'
import { get } from '../helpers/e2etest.js'
import { PREFERRED_LOCALE_COOKIE_NAME } from '../../lib/constants.js'
const langs = languageKeys.filter(lang => lang !== 'en')
describe('redirects', () => {
test.each(langs)('redirects to %s if accept-language', async (lang) => {
const res = await get('/get-started', {
headers: { 'accept-language': lang },
followRedirects: false,
})
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe(`/${lang}/get-started`)
expect(res.headers['cache-control']).toBe('private, no-store')
expect(res.headers['set-cookie']).toBeUndefined()
})
test.each(langs)('redirects to %s if PREFERRED_LOCALE_COOKIE_NAME', async (lang) => {
const res = await get('/get-started', {
headers: {
'accept-language': 'en',
Cookie: `${PREFERRED_LOCALE_COOKIE_NAME}=${lang}`,
},
followRedirects: false,
})
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe(`/${lang}/get-started`)
expect(res.headers['cache-control']).toBe('private, no-store')
expect(res.headers['set-cookie']).toBeUndefined()
})
test('redirects accidental /jp* requests to /ja*', async () => {
const res = await get('/jp')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('/ja')
})
test('redirects accidental /zh-CN* requests to /cn*', async () => {
const res = await get('/zh-CN')
expect(res.statusCode).toBe(301)
expect(res.headers.location).toBe('/cn')
})
})

View File

@@ -0,0 +1,13 @@
import { languageKeys } from '../../lib/languages'
import { get } from '../helpers/e2etest.js'
const langs = languageKeys.filter(lang => lang !== 'en')
// Skipping for now, as we would need to download the indexes with LFS
// in Actions to run these. Explore again after ES switch over.
describe.skip('search', () => {
test.each(langs)('search in %s', async (lang) => {
const res = await get(`/search?language=${lang}&version=dotcom&query=pages`)
expect(res.statusCode).toBe(200)
})
})

View File

@@ -1,22 +0,0 @@
import { expect } from '@jest/globals'
import { getLanguageCodeFromPath } from '../../middleware/detect-language.js'
describe('detect-language - getLanguageCodeFromPath', () => {
test('should handle client-side routing path shape', () => {
expect(getLanguageCodeFromPath('/_next/data/development/ja/articles/foo')).toBe('ja')
})
test('should not error on /_next/data/ input', () => {
expect(getLanguageCodeFromPath('/_next/data/')).toBe('en')
})
test('should return for paths with an extension', () => {
expect(getLanguageCodeFromPath('/ja.json')).toBe('ja')
expect(getLanguageCodeFromPath('/_next/data/development/ja.json')).toBe('ja')
})
test('should return en for invalid languages', () => {
expect(getLanguageCodeFromPath('/xx/articles/foo')).toBe('en')
expect(getLanguageCodeFromPath('/_next/data/development/xx/articles/foo')).toBe('en')
})
})

View File

@@ -8,25 +8,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
describe('find page', () => {
jest.setTimeout(1000 * 1000)
test("falls back to the English page if it can't find a localized page", async () => {
const page = await Page.init({
relativePath: 'page-that-does-not-exist-in-translations-dir.md',
basePath: path.join(__dirname, '../fixtures'),
languageCode: 'en',
})
const englishPermalink = page.permalinks[0].href
const japanesePermalink = englishPermalink.replace('/en/', '/ja/')
// add named keys
const pageMap = {
[englishPermalink]: page,
}
const localizedPage = findPage(japanesePermalink, pageMap, {})
expect(typeof localizedPage.title).toBe('string')
})
test('follows redirects', async () => {
const page = await Page.init({
relativePath: 'page-with-redirects.md',

View File

@@ -1,36 +1,8 @@
import { describe, expect, test } from '@jest/globals'
import { describe, expect } from '@jest/globals'
import getRedirect, { splitPathByLanguage } from '../../lib/get-redirect.js'
import getRedirect from '../../lib/get-redirect.js'
import { latest } from '../../lib/enterprise-server-releases.js'
describe('splitPathByLanguage', () => {
test('basic', () => {
const [language, withoutLanguage] = splitPathByLanguage('/foo/')
expect(language).toBe('en')
expect(withoutLanguage).toBe('/foo/')
})
test('already has /en in it', () => {
const [language, withoutLanguage] = splitPathByLanguage('/en/foo/')
expect(language).toBe('en')
expect(withoutLanguage).toBe('/foo/')
})
test('basic with different fallback', () => {
const [language, withoutLanguage] = splitPathByLanguage('/foo/', 'ja')
expect(language).toBe('ja')
expect(withoutLanguage).toBe('/foo/')
})
test('already has /en different fallback', () => {
const [language, withoutLanguage] = splitPathByLanguage('/en/foo/', 'ja')
expect(language).toBe('en')
expect(withoutLanguage).toBe('/foo/')
})
test('unrecognized prefix is ignored', () => {
const [language, withoutLanguage] = splitPathByLanguage('/sv/foo/')
expect(language).toBe('en')
expect(withoutLanguage).toBe('/sv/foo/')
})
})
describe('getRedirect basics', () => {
it('should sometimes not correct the version prefix', () => {
// This essentially tests legacy entries that come from the
@@ -153,20 +125,6 @@ describe('getRedirect basics', () => {
expect(getRedirect(`/en/enterprise-cloud@latest/user/foo`, ctx)).toBeUndefined()
})
it('should only inject language sometimes', () => {
const ctx = {
pages: {},
redirects: {
'/foo': '/bar',
},
}
// Nothing's needed here because it's not /admin/guides and
// it already has the enterprise-server prefix.
expect(getRedirect('/foo', ctx)).toBe('/en/bar')
expect(getRedirect('/en/foo', ctx)).toBe('/en/bar')
expect(getRedirect('/ja/foo', ctx)).toBe('/ja/bar')
})
it('should redirect both the prefix and the path needs to change', () => {
const ctx = {
pages: {},
@@ -179,20 +137,6 @@ describe('getRedirect basics', () => {
expect(getRedirect('/enterprise-server/foo', ctx)).toBe(`/en/enterprise-server@${latest}/bar`)
})
it('should redirect according to the req.context.userLanguage', () => {
const ctx = {
pages: {},
redirects: {
'/foo': '/bar',
},
userLanguage: 'ja',
}
expect(getRedirect('/foo', ctx)).toBe(`/ja/bar`)
// falls back to 'en' if it's falsy
ctx.userLanguage = null
expect(getRedirect('/foo', ctx)).toBe(`/en/bar`)
})
it('should work for some deprecated enterprise-server URLs too', () => {
// Starting with enterprise-server 3.0, we have made redirects become
// a *function* rather than a lookup on a massive object.

View File

@@ -1,19 +0,0 @@
import revalidator from 'revalidator'
import languages from '../../lib/languages.js'
import schema from '../helpers/schemas/languages-schema.js'
describe('languages module', () => {
test('is an object with language codes as keys', () => {
expect('en' in languages).toBe(true)
expect('ja' in languages).toBe(true)
expect('cn' in languages).toBe(true)
})
test('every language is valid', () => {
Object.values(languages).forEach((language) => {
const { valid, errors } = revalidator.validate(language, schema)
const expectation = JSON.stringify(errors, null, 2)
expect(valid, expectation).toBe(true)
})
})
})

View File

@@ -15,7 +15,6 @@ describe('liquid helper tags', () => {
context.pages = pageMap
context.redirects = {
'/en/desktop/contributing-and-collaborating-using-github-desktop': `/en/${nonEnterpriseDefaultVersion}/desktop/contributing-and-collaborating-using-github-desktop`,
'/ja/desktop/contributing-and-collaborating-using-github-desktop': `/ja/${nonEnterpriseDefaultVersion}/desktop/contributing-and-collaborating-using-github-desktop`,
'/en/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories': `/en/${nonEnterpriseDefaultVersion}/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories`,
'/en/github/writing-on-github/basic-writing-and-formatting-syntax': `/en/${nonEnterpriseDefaultVersion}/github/writing-on-github/basic-writing-and-formatting-syntax`,
}

View File

@@ -1,60 +0,0 @@
import path from 'path'
import { fileURLToPath } from 'url'
import { expect } from '@jest/globals'
import languages from '../../lib/languages.js'
import Page from '../../lib/page.js'
import { loadPageMap, correctTranslationOrphans } from '../../lib/page-data.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
describe('loading page map with translation orphans', () => {
test('inject missing translations from English', async () => {
const basePath = path.join(__dirname, '../fixtures')
const page = await Page.init({
relativePath: 'page-that-does-not-exist-in-translations-dir.md',
basePath,
languageCode: 'en',
})
console.assert(page, 'page could not be loaded')
const pageList = [page]
const pageMap = await loadPageMap(await correctTranslationOrphans(pageList, basePath))
// It should make a copy of the English into each language
expect(Object.keys(pageMap).length).toBe(Object.keys(languages).length)
// +1 for the test just above, 2 tests per language.
expect.assertions(1 + Object.keys(languages).length * 2)
for (const languageCode of Object.keys(languages)) {
for (const permalink of page.permalinks) {
const translationHref = permalink.href.replace('/en', `/${languageCode}`)
const translationPage = pageMap[translationHref]
expect(translationPage).toBeTruthy()
expect(translationPage.languageCode).toBe(languageCode)
}
}
})
test('remove translation pages that were not in English', async () => {
const basePath = path.join(__dirname, '../fixtures')
const page = await Page.init({
relativePath: 'page-that-does-not-exist-in-translations-dir.md',
basePath,
languageCode: 'en',
})
console.assert(page, 'page could not be loaded')
const orphan = await Page.init({
relativePath: 'article-with-videos.md',
basePath,
languageCode: 'ja',
})
console.assert(orphan, 'page could not be loaded')
const orphanPermalinks = orphan.permalinks.map((p) => p.href)
const pageList = await correctTranslationOrphans([page, orphan], basePath)
const pageMap = await loadPageMap(pageList)
expect(pageMap[orphanPermalinks[0]]).toBeFalsy()
expect(Object.keys(pageMap).length).toBe(Object.keys(languages).length)
})
})

View File

@@ -31,15 +31,6 @@ describe('Page class', () => {
expect(page.fullPath.includes(page.relativePath)).toBe(true)
})
test('does not error out on translated TOC with no links', async () => {
const page = await Page.init({
relativePath: 'translated-toc-with-no-links-index.md',
basePath: path.join(__dirname, '../fixtures'),
languageCode: 'ja',
})
expect(typeof page.title).toBe('string')
})
describe('showMiniToc page property', () => {
let article, articleWithFM, tocPage
@@ -612,17 +603,6 @@ describe('Page class', () => {
it('works for the homepage', () => {
const variants = Page.getLanguageVariants('/en')
expect(variants.find(({ code }) => code === 'en').href).toBe('/en')
// expect(variants.find(({ code }) => code === 'ja').href).toBe('/ja')
})
it('works for enterprise URLs', () => {
const variants = Page.getLanguageVariants(
`/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user/articles/github-glossary`
)
expect(variants.find(({ code }) => code === 'en').href).toBe(
`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/articles/github-glossary`
)
// expect(variants.find(({ code }) => code === 'ja').href).toBe('/ja/enterprise/2.14/user/articles/github-glossary')
})
})

View File

@@ -6,24 +6,12 @@ import { liquid } from '../../lib/render-content/index.js'
import patterns from '../../lib/patterns.js'
import GithubSlugger from 'github-slugger'
import { decode } from 'html-entities'
import walk from 'walk-sync'
import { chain, difference, pick } from 'lodash-es'
import { chain, pick } from 'lodash-es'
import checkIfNextVersionOnly from '../../lib/check-if-next-version-only.js'
import removeFPTFromPath from '../../lib/remove-fpt-from-path.js'
const languageCodes = Object.keys(libLanguages)
const slugger = new GithubSlugger()
// By default, the tests don't change that each translation has an
// equivalent English page (e.g. `translations/*/content/foo.md`
// expects `content/foo.md`)
// Set the environment variable `TEST_TRANSLATION_MATCHING=true`
// to enable that test.
const testIfRequireTranslationMatching = JSON.parse(
process.env.TEST_TRANSLATION_MATCHING || 'false'
)
? test
: test.skip
describe('pages module', () => {
jest.setTimeout(60 * 1000)
@@ -163,30 +151,6 @@ describe('pages module', () => {
const failureMessage = JSON.stringify(liquidErrors, null, 2)
expect(liquidErrors.length, failureMessage).toBe(0)
})
testIfRequireTranslationMatching(
'every non-English page has a matching English page',
async () => {
const englishPaths = chain(walk('content', { directories: false }))
.uniq()
.value()
const nonEnglishPaths = chain(Object.values(libLanguages))
.filter((language) => language.code !== 'en')
.map((language) => walk(`${language.dir}/content`, { directories: false }))
.flatten()
.uniq()
.value()
const diff = difference(nonEnglishPaths, englishPaths)
const failureMessage = `
Found ${diff.length} non-English pages without a matching English page:\n - ${diff.join('\n - ')}
Remove them with script/i18n/prune-stale-files.js and commit your changes using "git commit --no-verify".
`
expect(diff.length, failureMessage).toBe(0)
}
)
})
describe('loadPageMap', () => {

View File

@@ -1,4 +1,4 @@
import parseUserAgent from '../../components/lib/user-agent'
import { parseUserAgent } from '../../components/lib/user-agent.ts'
describe('parseUserAgent', () => {
it('android, chrome', () => {