Files
jackwener__opencli/clis/51job/utils.test.js
jakevin 3ec98b9405 feat(51job): comprehensive adapter (search / hot / detail / company) (#1132)
* feat(51job): add comprehensive 51job adapter (search / hot / detail / company)

Four adapters covering the main 51job surface:

- `51job search <keyword>` — keyword job search via we.51job.com/api/job/search-pc.
  Rich filters: --area (40+ city name/alias → 6-digit code), --salary, --experience,
  --degree, --companyType, --companySize, --sort, --page, --limit. Response already
  carries full jobDescribe + HR + company + encCoId, so most callers won't need detail.

- `51job hot` — same endpoint with empty keyword, returns 51job's recommendation feed.

- `51job detail <jobId>` — scrapes jobs.51job.com/x/<jobId>.html. Returns description,
  welfare tags, category, address, age requirement, company meta.

- `51job company <encCoId>` — scrapes jobs.51job.com/all/co<encCoId>.html. Job cards
  carry a `sensorsdata` JSON attribute, so we parse that instead of fragile DOM text.
  Company meta from `.c-info.ellipsis`, intro from `#companyIntroRef`.

All four are Strategy.COOKIE + browser:true + navigateBefore:false. 51job sits
behind Aliyun WAF — bare curl / Node-side fetch always hits the slider challenge
(tried copying acw_sc__v2 + ssxmod_itna cookies to Node, WAF also checks TLS
fingerprint and JS execution). Only reliable path is browser-context fetch via
`page.evaluate(fetch(url, {credentials:'include'}))`, so utils.js exports
`pageFetchJson` that wraps this pattern + detects WAF-served HTML.

Verify fixtures included (~/.opencli/sites/51job/verify/*.json) — four adapters
pass `opencli browser verify 51job/<cmd>` with rowCount / columns / types /
patterns / notEmpty checks. Eyeballed jobId 171699769 on jobs.51job.com/suzhou
matches adapter output.

* fix(51job): tighten city handling and docs

* chore: regenerate cli-manifest.json after 51job column cleanup
2026-04-22 13:16:23 +08:00

70 lines
2.4 KiB
JavaScript

import { describe, expect, it, vi } from 'vitest';
import { CliError } from '@jackwener/opencli/errors';
import { parseCompanyJobCard, pageFetchJson, resolveCity } from './utils.js';
describe('51job resolveCity', () => {
it('maps known city names and explicit national scope', () => {
expect(resolveCity('杭州')).toBe('080200');
expect(resolveCity('all')).toBe('000000');
expect(resolveCity('000000')).toBe('000000');
});
it('rejects unknown non-empty inputs instead of silently widening to 全国', () => {
expect(() => resolveCity('杭州z')).toThrowError(CliError);
expect(() => resolveCity('杭州z')).toThrow(/Unknown city\/area/);
});
});
describe('51job pageFetchJson', () => {
it('detects WAF challenge HTML and throws ANTI_BOT', async () => {
const page = {
evaluate: vi.fn().mockResolvedValue({
ok: true,
status: 200,
text: '<html><title>slider</title></html>',
}),
};
await expect(pageFetchJson(page, 'https://we.51job.com/api/job/search-pc')).rejects.toMatchObject({
code: 'ANTI_BOT',
});
});
});
describe('51job parseCompanyJobCard', () => {
it('parses sensorsdata JSON into a stable row fragment', () => {
const row = parseCompanyJobCard({
href: 'https://jobs.51job.com/shanghai/123456789.html',
sensorsdata: JSON.stringify({
jobId: '123456789',
jobTitle: 'Senior Engineer',
jobSalary: '20-30K',
jobArea: '上海',
jobYear: '3-5年',
jobDegree: '本科',
funcType: '后端开发',
jobTime: '04-22',
}),
});
expect(row).toEqual({
jobId: '123456789',
title: 'Senior Engineer',
salary: '20-30K',
city: '上海',
workYear: '3-5年',
degree: '本科',
funcType: '后端开发',
issueDate: '04-22',
url: 'https://jobs.51job.com/shanghai/123456789.html',
});
});
it('returns null on malformed sensorsdata', () => {
expect(parseCompanyJobCard({
href: 'https://jobs.51job.com/shanghai/123456789.html',
sensorsdata: '{bad json}',
})).toBeNull();
});
});