frigatebird 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/README.md +12 -6
- package/dist/client/playwright-client.js +86 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 - 2026-02-09
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Premium-feature live e2e opt-in flag: `--enable-premium-features-e2e`.
|
|
7
|
+
- Live e2e support for dedicated premium account cookies via `--article-cookie-source`.
|
|
8
|
+
- Coverage thresholds in `vitest.config.ts` to enforce release quality gates.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Live mutation CI now accepts `workflow_dispatch` input `list_name` instead of a repository variable.
|
|
12
|
+
- Premium feature mutation coverage (article publish path) is disabled by default and only runs when explicitly enabled.
|
|
13
|
+
- Article publish automation was updated for current X compose flows (`/compose/articles`, `Write`, and modern composer/title selectors).
|
|
14
|
+
- Live e2e naming now reflects premium-feature scope instead of article-specific semantics.
|
|
15
|
+
|
|
16
|
+
### Tests
|
|
17
|
+
- Added/updated Playwright client tests for modern article composer fallback behavior.
|
|
18
|
+
- Verified full release gate (`lint`, `build`, `coverage`) and fixture e2e pass on 0.3.1 candidate.
|
|
19
|
+
|
|
3
20
|
## 0.3.0 - 2026-02-09
|
|
4
21
|
|
|
5
22
|
### Added
|
|
@@ -12,7 +29,7 @@
|
|
|
12
29
|
- Mutation `retweet` flow now uses bounded resilient click handling and explicit completion checks.
|
|
13
30
|
- Read-only fixture e2e was optimized to run significantly faster while preserving command coverage.
|
|
14
31
|
- CI and release verification workflows now include fixture e2e smoke runs.
|
|
15
|
-
- Live mutation e2e now requires `
|
|
32
|
+
- Live mutation e2e now requires a `--list-name` argument and always validates list `add/remove/batch` paths.
|
|
16
33
|
|
|
17
34
|
### Tests
|
|
18
35
|
- Expanded unit coverage for session identity parsing and retweet mutation branches.
|
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frigatebird 🐦 — resilient X CLI for posting, articles, replies, reading, and list automation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div style="text-align: center;">
|
|
4
|
+
<img src="images/frigatebird_logo.jpg" alt="Frigatebird logo" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
4
7
|
|
|
5
8
|
`frigatebird` is a Playwright-first X CLI that preserves the familiar `bird` command-line experience while running on browser automation instead of private GraphQL internals.
|
|
6
9
|
|
|
@@ -198,21 +201,24 @@ npm run lint
|
|
|
198
201
|
npm run test:run # unit/integration
|
|
199
202
|
npm run test:coverage # unit/integration + coverage
|
|
200
203
|
npm run test:e2e # e2e only
|
|
201
|
-
npm run test:e2e:live # opt-in live mutation e2e (
|
|
204
|
+
npm run test:e2e:live -- --list-name testlist001 # opt-in live mutation e2e (article mutation disabled by default)
|
|
202
205
|
npm run smoke:pack-install
|
|
203
206
|
```
|
|
204
207
|
|
|
205
|
-
Live mutation e2e
|
|
208
|
+
Live mutation e2e requirements:
|
|
209
|
+
- required argument: `--list-name <your-list-name>`
|
|
206
210
|
- `FRIGATEBIRD_AUTH_TOKEN`
|
|
207
211
|
- `FRIGATEBIRD_CT0`
|
|
208
|
-
-
|
|
209
|
-
- optional: `FRIGATEBIRD_LIVE_COOKIE_SOURCE`, `FRIGATEBIRD_LIVE_EXPECTED_HANDLE_PREFIX`, `FRIGATEBIRD_LIVE_TARGET_HANDLE`
|
|
212
|
+
- optional args: `--cookie-source <source>`
|
|
213
|
+
- optional env: `FRIGATEBIRD_LIVE_COOKIE_SOURCE`, `FRIGATEBIRD_LIVE_EXPECTED_HANDLE_PREFIX`, `FRIGATEBIRD_LIVE_TARGET_HANDLE`
|
|
214
|
+
- article mutation is disabled by default; enable manually with:
|
|
215
|
+
- `--enable-premium-features-e2e --article-cookie-source chrome --article-expected-handle-prefix Oceanswave`
|
|
210
216
|
|
|
211
217
|
## Release
|
|
212
218
|
|
|
213
219
|
- CI: `.github/workflows/ci.yml`
|
|
214
220
|
- npm publish: `.github/workflows/release.yml` (triggered by GitHub Release `published`, trusted publishing/OIDC)
|
|
215
|
-
- live mutation CI: `.github/workflows/live-e2e.yml` (manual trigger;
|
|
221
|
+
- live mutation CI: `.github/workflows/live-e2e.yml` (manual trigger; accepts `list_name` workflow input + validates auth secrets before running)
|
|
216
222
|
- release checklist: `RELEASE.md`
|
|
217
223
|
|
|
218
224
|
## Notes
|
|
@@ -319,6 +319,26 @@ export class PlaywrightXClient {
|
|
|
319
319
|
throw new Error("Composer text area not found. X may have changed selectors.");
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
|
+
async recoverComposerFromHome(page, timeoutMs = 12000) {
|
|
323
|
+
await page.goto(this.absolute("/home"), {
|
|
324
|
+
waitUntil: "domcontentloaded",
|
|
325
|
+
});
|
|
326
|
+
await this.dismissBlockingLayers(page);
|
|
327
|
+
const composeSelectors = [
|
|
328
|
+
'[data-testid="SideNav_NewTweet_Button"]',
|
|
329
|
+
'[data-testid="FloatingActionButton_Tweet"]',
|
|
330
|
+
'a[href="/compose/post"]',
|
|
331
|
+
'[role="button"][aria-label*="Post"]',
|
|
332
|
+
];
|
|
333
|
+
await this.clickFirstVisible(page, composeSelectors, 4000).catch(() => false);
|
|
334
|
+
try {
|
|
335
|
+
await this.waitForComposer(page, timeoutMs);
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
322
342
|
async fillComposer(page, text) {
|
|
323
343
|
const composerSelectors = [
|
|
324
344
|
'[data-testid="tweetTextarea_0"]',
|
|
@@ -696,7 +716,17 @@ export class PlaywrightXClient {
|
|
|
696
716
|
await page.goto(this.absolute("/compose/post"), {
|
|
697
717
|
waitUntil: "domcontentloaded",
|
|
698
718
|
});
|
|
699
|
-
await this.
|
|
719
|
+
await this.dismissBlockingLayers(page);
|
|
720
|
+
let composerReady = true;
|
|
721
|
+
try {
|
|
722
|
+
await this.waitForComposer(page, 12000);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
composerReady = await this.recoverComposerFromHome(page, 12000);
|
|
726
|
+
}
|
|
727
|
+
if (!composerReady) {
|
|
728
|
+
throw new Error("Composer text area not found. X may have changed selectors.");
|
|
729
|
+
}
|
|
700
730
|
await this.fillComposer(page, text);
|
|
701
731
|
await this.attachMedia(page, mediaSpecs);
|
|
702
732
|
await this.submitComposer(page, "tweet");
|
|
@@ -717,6 +747,7 @@ export class PlaywrightXClient {
|
|
|
717
747
|
return this.withPage(async (page) => {
|
|
718
748
|
await this.ensureAuth(page);
|
|
719
749
|
const composePaths = [
|
|
750
|
+
"/compose/articles",
|
|
720
751
|
"/i/articles/new",
|
|
721
752
|
"/compose/article",
|
|
722
753
|
"/write",
|
|
@@ -731,19 +762,36 @@ export class PlaywrightXClient {
|
|
|
731
762
|
'[role="textbox"][aria-label*="Title"]',
|
|
732
763
|
'input[placeholder*="Title"]',
|
|
733
764
|
'textarea[placeholder*="Title"]',
|
|
765
|
+
'input[placeholder*="title" i]',
|
|
766
|
+
'textarea[placeholder*="title" i]',
|
|
767
|
+
'textarea[placeholder*="Add a title"]',
|
|
734
768
|
'[contenteditable="true"][aria-label*="Title"]',
|
|
735
769
|
];
|
|
736
770
|
const bodySelectors = [
|
|
737
771
|
'[data-testid="articleBodyInput"]',
|
|
738
772
|
'[data-testid="article-editor"] [contenteditable="true"]',
|
|
773
|
+
'[data-testid="composer"][role="textbox"]',
|
|
774
|
+
'[data-testid="composer"]',
|
|
739
775
|
'.ProseMirror[contenteditable="true"]',
|
|
740
776
|
'[role="textbox"][aria-label*="Write"]',
|
|
741
777
|
'[contenteditable="true"][aria-label*="Write"]',
|
|
742
778
|
'[role="textbox"][aria-label*="Body"]',
|
|
743
779
|
'textarea[placeholder*="Write"]',
|
|
744
780
|
];
|
|
781
|
+
const combinedComposerSelectors = [
|
|
782
|
+
'[data-testid="composer"][role="textbox"]',
|
|
783
|
+
'[data-testid="composer"]',
|
|
784
|
+
'[contenteditable="true"][data-testid="composer"]',
|
|
785
|
+
];
|
|
786
|
+
const writeSelectors = [
|
|
787
|
+
'[data-testid="empty_state_button_text"]:has-text("Write")',
|
|
788
|
+
'[role="button"]:has-text("Write")',
|
|
789
|
+
'[role="link"]:has-text("Write")',
|
|
790
|
+
'[role="button"]:has-text("New article")',
|
|
791
|
+
];
|
|
745
792
|
let titleField = null;
|
|
746
793
|
let bodyField = null;
|
|
794
|
+
let combinedComposer = null;
|
|
747
795
|
for (const path of composePaths) {
|
|
748
796
|
await page
|
|
749
797
|
.goto(this.absolute(path), { waitUntil: "domcontentloaded" })
|
|
@@ -754,25 +802,54 @@ export class PlaywrightXClient {
|
|
|
754
802
|
'[role="button"]:has-text("Write article")',
|
|
755
803
|
'[data-testid="articleComposerButton"]',
|
|
756
804
|
'a[href*="/i/articles/new"]',
|
|
805
|
+
'a[href*="/compose/articles"]',
|
|
806
|
+
'[role="link"]:has-text("Articles")',
|
|
757
807
|
], 1500).catch(() => false);
|
|
808
|
+
await this.clickFirstVisible(page, writeSelectors, 1500).catch(() => false);
|
|
758
809
|
titleField = await this.firstVisibleLocator(page, titleSelectors, 3500, 120);
|
|
759
810
|
bodyField = await this.firstVisibleLocator(page, bodySelectors, 3500, 120);
|
|
760
|
-
|
|
811
|
+
combinedComposer = await this.firstVisibleLocator(page, combinedComposerSelectors, 2500, 120);
|
|
812
|
+
if ((titleField && bodyField) || combinedComposer) {
|
|
761
813
|
break;
|
|
762
814
|
}
|
|
763
815
|
}
|
|
764
816
|
if (!titleField || !bodyField) {
|
|
765
|
-
|
|
817
|
+
if (combinedComposer) {
|
|
818
|
+
await combinedComposer.click().catch(() => { });
|
|
819
|
+
await combinedComposer.fill(`${cleanTitle}\n\n${cleanBody}`);
|
|
820
|
+
const inserted = await combinedComposer
|
|
821
|
+
.evaluate((node) => {
|
|
822
|
+
const element = node;
|
|
823
|
+
return (element.innerText ||
|
|
824
|
+
element.textContent ||
|
|
825
|
+
element.value ||
|
|
826
|
+
"").trim();
|
|
827
|
+
})
|
|
828
|
+
.catch(() => "");
|
|
829
|
+
if (inserted.length < Math.min(8, cleanTitle.length) ||
|
|
830
|
+
!inserted
|
|
831
|
+
.toLowerCase()
|
|
832
|
+
.includes(cleanBody.slice(0, 8).toLowerCase())) {
|
|
833
|
+
await page.keyboard
|
|
834
|
+
.type(`${cleanTitle}\n\n${cleanBody}`)
|
|
835
|
+
.catch(() => { });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
return fail("Could not locate article composer. Articles may be unavailable for this account or X changed selectors.");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
await titleField.click().catch(() => { });
|
|
844
|
+
await titleField.fill(cleanTitle);
|
|
845
|
+
await bodyField.click().catch(() => { });
|
|
846
|
+
await bodyField.fill(cleanBody);
|
|
766
847
|
}
|
|
767
|
-
await titleField.click().catch(() => { });
|
|
768
|
-
await titleField.fill(cleanTitle);
|
|
769
|
-
await bodyField.click().catch(() => { });
|
|
770
|
-
await bodyField.fill(cleanBody);
|
|
771
848
|
const published = await this.clickFirstVisible(page, [
|
|
772
849
|
'[data-testid="articlePublishButton"]:not([aria-disabled="true"])',
|
|
773
850
|
'[data-testid="publishButton"]:not([aria-disabled="true"])',
|
|
774
|
-
'[role="button"]:has-text("Publish"):not([aria-disabled="true"])',
|
|
775
|
-
'[role="button"]:has-text("Post"):not([aria-disabled="true"])',
|
|
851
|
+
'[role="button"]:has-text("Publish"):not([aria-disabled="true"]):not([disabled])',
|
|
852
|
+
'[role="button"]:has-text("Post"):not([aria-disabled="true"]):not([disabled])',
|
|
776
853
|
], 12000).catch(() => false);
|
|
777
854
|
if (!published) {
|
|
778
855
|
return fail("Could not locate article publish button.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frigatebird",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Playwright-first X CLI with bird-style parity and resilient web automation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"test:run": "vitest run --config vitest.config.ts",
|
|
43
43
|
"test:coverage": "vitest run --config vitest.config.ts --coverage",
|
|
44
44
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
45
|
-
"test:e2e:live": "
|
|
45
|
+
"test:e2e:live": "tsx scripts/run-live-e2e.ts",
|
|
46
46
|
"lint": "biome check .",
|
|
47
47
|
"lint:fix": "biome check --write .",
|
|
48
48
|
"release:check": "npm run lint && npm run build && npm run test:coverage",
|