import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); const styles = fs.readFileSync(new URL('../styles.css', import.meta.url), 'utf8'); const sandbox = { console, document: { addEventListener() {}, getElementById() { return null; } }, window: {}, pinyinPro: { pinyin(value) { return value === '广州合作站' ? 'guang zhou he zuo zhan' : value; } }, navigator: {}, }; sandbox.globalThis = sandbox; vm.runInNewContext(source, sandbox, { filename: 'app.js' }); assert.equal(sandbox.stationSearchScore({ name: '广州合作站' }, 'gz'), 2); assert.equal(sandbox.stationSearchScore({ address: '广东省广州市黄埔区' }, '黄埔'), 3); assert.equal(sandbox.stationSearchScore({ name: '广州合作站' }, '北京'), 0); assert.equal(sandbox.hasCoordinates({ longitude: 113.2, latitude: 23.1 }), true); assert.equal(sandbox.hasCoordinates({ longitude: 'bad', latitude: 23.1 }), false); vm.runInNewContext(` state.stations = [ { id: 'partner', name: '广州合作站', cooperative: true }, { id: 'external', name: '广州外部站', cooperative: false } ]; globalThis.allStationIds = filteredStations().map(station => station.id); state.stationFilter = 'partner'; globalThis.partnerStationIds = filteredStations().map(station => station.id); `, sandbox); assert.equal(sandbox.allStationIds.join(','), 'partner,external'); assert.equal(sandbox.partnerStationIds.join(','), 'partner'); vm.runInNewContext(` state.stations = [ { id: 'gd-hp', name: '黄埔合作站', province: '广东省', city: '广州市', district: '黄埔区', longitude: 113.3, latitude: 23.1, cooperative: true }, { id: 'gd-nh', name: '南海合作站', province: '广东省', city: '佛山市', district: '南海区', longitude: 113.1, latitude: 23.0, cooperative: true }, { id: 'zj-jx', name: '嘉兴合作站', province: '浙江省', city: '嘉兴市', district: '秀洲区', longitude: 120.7, latitude: 30.7, cooperative: true } ]; globalThis.hierarchy = [stationHierarchyLevel(4.8), stationHierarchyLevel(8), stationHierarchyLevel(13)]; globalThis.provinceNodeCount = buildStationNodes(filteredStations(), 'province').length; globalThis.cityNodeCount = buildStationNodes(filteredStations(), 'city').length; globalThis.stationNodeCount = buildStationNodes(filteredStations(), 'station').length; globalThis.locationOptionCount = stationLocationOptions().length; globalThis.provinceOptionScope = stationLocationOptions().find(option => option.level === 'province' && option.province === '广东省'); globalThis.cityOptionScope = stationLocationOptions().find(option => option.level === 'city' && option.city === '广州市'); `, sandbox); assert.equal(sandbox.hierarchy.join(','), 'province,city,station'); assert.equal(sandbox.provinceNodeCount, 2); assert.equal(sandbox.cityNodeCount, 3); assert.equal(sandbox.stationNodeCount, 3); assert.equal(sandbox.locationOptionCount, 8); assert.equal(sandbox.provinceOptionScope.city, ''); assert.equal(sandbox.provinceOptionScope.district, ''); assert.equal(sandbox.cityOptionScope.district, ''); vm.runInNewContext(` state.regionPicker = { level: 'province', province: '', city: '', search: '', draft: [], options: [] }; globalThis.regionProvinceLabels = regionPickerOptions().map(option => option.label).sort().join(','); state.regionPicker = { level: 'city', province: '广东省', city: '', search: '', draft: [], options: [] }; globalThis.regionCityLabels = regionPickerOptions().map(option => option.label).sort().join(','); state.regionPicker = { level: 'province', province: '', city: '', search: '嘉兴', draft: [], options: [] }; globalThis.regionSearchLabels = regionPickerOptions().map(option => option.label).join(','); `, sandbox); assert.equal(sandbox.regionProvinceLabels, '广东,浙江'); assert.equal(sandbox.regionCityLabels, '佛山,广州'); assert.match(sandbox.regionSearchLabels, /嘉兴/); vm.runInNewContext(` state.stations = [ { id: 'sh-hp', name: '上海黄浦站', province: '上海市', city: '上海市', district: '黄浦区', longitude: 121.5, latitude: 31.2, cooperative: true }, { id: 'sh-pd', name: '上海浦东站', province: '上海市', city: '上海市', district: '浦东新区', longitude: 121.6, latitude: 31.2, cooperative: true }, { id: 'gd-gz', name: '广州站', province: '广东省', city: '广州市', district: '天河区', longitude: 113.3, latitude: 23.1, cooperative: true }, { id: 'zj-hz-1', name: '杭州站一', province: '浙江省', city: '杭州市', district: '西湖区', longitude: 120.1, latitude: 30.2, cooperative: true }, { id: 'zj-hz-2', name: '杭州站二', province: '浙江省', city: '杭州市', district: '上城区', longitude: 120.2, latitude: 30.2, cooperative: true }, { id: 'zj-hz-3', name: '杭州站三', province: '浙江省', city: '杭州市', district: '拱墅区', longitude: 120.3, latitude: 30.2, cooperative: true } ]; globalThis.shanghaiCityOption = stationLocationOptions().find(option => option.level === 'city' && option.province === '上海市'); state.regionPicker = { level: 'province', province: '', city: '', search: '', draft: [], options: [] }; globalThis.primaryRegionLabels = regionPickerOptions().map(option => option.label).join(','); state.regionPicker = { level: 'city', province: '上海市', city: '', search: '', draft: [], options: [] }; globalThis.shanghaiDistrictLabels = regionPickerOptions().map(option => option.label).sort().join(','); globalThis.shanghaiPath = compactPath(['上海市', '上海市', '黄浦区']); globalThis.shanghaiRegion = stationRegion(state.stations[0]); globalThis.directMunicipalityFlags = ['北京市', '上海市', '天津市', '重庆市'].map(isMunicipalityProvince).join(','); `, sandbox); assert.equal(sandbox.shanghaiCityOption, undefined); assert.equal(sandbox.primaryRegionLabels, '浙江,上海,广东'); assert.equal(sandbox.shanghaiDistrictLabels, '浦东新区,黄浦区'); assert.equal(sandbox.shanghaiPath, '上海市 · 黄浦区'); assert.equal(sandbox.shanghaiRegion, '上海市 · 黄浦区'); assert.equal(sandbox.directMunicipalityFlags, 'true,true,true,true'); vm.runInNewContext(` globalThis.inferredDistrict = stationDistrictName({ province: '浙江省', city: '嘉兴市', address: '嘉兴市海盐县海盐经济开发区' }); state.locationFilters = []; state.query = '广州'; globalThis.queryMatches = stationMatchesQuery({ name: '广州合作站' }); globalThis.queryMisses = stationMatchesQuery({ name: '杭州合作站' }); `, sandbox); assert.equal(sandbox.inferredDistrict, '海盐县'); assert.equal(sandbox.queryMatches, true); assert.equal(sandbox.queryMisses, false); vm.runInNewContext(` state.query = ''; state.locationFilters = [ { level: 'city', province: '广东省', city: '广州市', district: '', label: '广州' }, { level: 'city', province: '浙江省', city: '嘉兴市', district: '', label: '嘉兴' } ]; globalThis.multiLocationMatches = [ stationMatchesLocation({ province: '广东省', city: '广州市', district: '白云区' }), stationMatchesLocation({ province: '浙江省', city: '嘉兴市', district: '海盐县' }), stationMatchesLocation({ province: '广东省', city: '深圳市', district: '南山区' }) ]; `, sandbox); assert.equal(sandbox.multiLocationMatches.join(','), 'true,true,false'); assert.ok(Math.abs(sandbox.distanceKm([113, 23], [114, 23]) - 102.4) < 1); assert.equal(sandbox.stationUnitPrice({ unitPrice: 12.5 }), '¥12.50 / kg'); assert.equal(sandbox.stationUnitPrice({ unitPrice: 0 }), ''); assert.match(sandbox.detailPhoneField('联系方式', '138 0000 0000'), /href="tel:13800000000"/); vm.runInNewContext(` const detailNodes = { detailName: { textContent: '' }, detailRegion: { textContent: '' }, detailType: { textContent: '', classList: { toggle() {} } }, detailFields: { innerHTML: '' }, stationDetail: { hidden: true } }; document.getElementById = id => detailNodes[id] || null; state.map = null; state.selected = null; selectStation({ id: 'partner-detail', name: '合作站', cooperative: true, province: '广东省', city: '广州市', district: '黄埔区', longitude: 113.3, latitude: 23.1, contactPerson: '张工', contactPhone: '138 0000 0000', unitPrice: 12.5 }); globalThis.partnerDetailHtml = detailNodes.detailFields.innerHTML; selectStation({ id: 'external-detail', name: '外部站', cooperative: false, province: '广东省', city: '广州市', district: '黄埔区', longitude: 113.4, latitude: 23.1, contactPerson: '李工', contactPhone: '13900000000', unitPrice: 10.5 }); globalThis.externalDetailHtml = detailNodes.detailFields.innerHTML; `, sandbox); assert.match(sandbox.partnerDetailHtml, /张工/); assert.match(sandbox.partnerDetailHtml, /tel:13800000000/); assert.match(sandbox.partnerDetailHtml, /¥12\.50 \/ kg/); assert.doesNotMatch(sandbox.externalDetailHtml, /李工|13900000000|¥10\.50/); assert.match(source, /zoom < 11\) return 'city'/); assert.match(source, /state\.userLocation && left\.kind === 'station'/); assert.doesNotMatch(source, /rankSecondaryTab|setRank\(/); assert.match(source, /stationSearchSuggestions/); assert.match(source, /handleStationSearchInput/); assert.match(source, /locationFilters/); assert.match(source, /renderLocationFilterChips/); assert.match(source, /openRegionPicker/); assert.match(source, /kpiTotal|kpiPartner/); const index = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'); assert.match(index, /station-summary-strip/); assert.match(index, /station-status-card/); assert.equal((index.match(/id="statusTotal"/g) || []).length, 1); assert.equal((index.match(/id="mobileStatusTotal"/g) || []).length, 1); assert.match(source, /window\.setTimeout\(\(\) => \{ state\.query = value/); assert.match(source, /focusSearchResults\(\)/); assert.match(source, /visibleOnly && !normalize\(state\.query\)/); assert.match(source, /station-list-partner-tag/); assert.match(source, /station-list-partner-tag\$\{node\.cooperative \? '' : ' is-placeholder'\}/); assert.match(source, /const rowValue = Number\.isFinite\(distance\)/); assert.match(source, /node\.cooperative \? 'H₂ · 合作' : ''/); assert.doesNotMatch(source, /在线 \$\{formatNumber\(node\.online\)\}/); assert.doesNotMatch(source, /monthlyHydrogenKg|totalHydrogenKg|vehicle/); assert.match(source, /uri\.amap\.com\/navigation/); assert.match(source, /window\.open\('', '_blank'\)/); assert.doesNotMatch(source, /window\.location\.assign/); assert.equal((source.match(/function navigateToStation\(/g) || []).length, 1); assert.match(source, /station-list-price/); assert.match(styles, /station-summary-strip/); assert.match(styles, /header-kpi-group,\n \.station-navigation-shell \.station-status-card/); assert.match(styles, /theme-switcher-bw \.bw-btn span \{ display: none; \}/); assert.match(styles, /height: clamp\(430px, 58dvh, 540px\)/); assert.match(styles, /map-action-controls \.glass-btn:nth-child\(2\)/); console.log('station navigation app tests: ok');