Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e1b9dd8
fix(chat): hide agent built-in tools from the activity view (backport…
Navin-S-R Sep 2, 2026
6f2ab59
ci: adopt version-15-hotfix; stable only takes release PRs; bump to 1…
kavin-114 Sep 3, 2026
adf82d7
ci: release guard in its own workflow; version-agnostic release-notic…
kavin-114 Sep 3, 2026
1aff893
tests: www release-notice test derives its version from the installed…
kavin-114 Sep 3, 2026
2536697
ci: release guard runs from the base branch and checks the version bump
kavin-114 Sep 3, 2026
953c4e5
ci: run the matrix on base retargets; guard hardening; sentinel test …
kavin-114 Sep 3, 2026
4a02fbc
ci: drop `edited` from ci.yml; guard fails a retargeted PR until re-run
kavin-114 Sep 3, 2026
1660f4e
Merge pull request #1091 from aerele/ci/hotfix-branch-model-version-15
venkat102 Sep 3, 2026
4f901f4
Merge pull request #1079 from aerele/backport/1071-hide-noio-builtin-…
kavin-114 Sep 3, 2026
6f9e7d0
ci: tag and publish a GitHub Release automatically when a stable bran…
kavin-114 Sep 3, 2026
5087617
ci: release workflow resumes after a partial failure; strict branch c…
kavin-114 Sep 3, 2026
81445c2
ci: release workflow header matches the tag ruleset GitHub actually a…
kavin-114 Sep 3, 2026
2c3a264
Merge pull request #1100 from aerele/backport/release-automation-vers…
kavin-114 Sep 3, 2026
e00b0b3
Merge pull request #1102 from aerele/fix/desk-nudge-dynamic-cta
kavin-114 Sep 3, 2026
24dd0f8
Merge pull request #1103 from aerele/feat/support-header-count-pill
kavin-114 Sep 3, 2026
36a9ff6
Merge pull request #1104 from aerele/feat/schedule-day-picker
kavin-114 Sep 3, 2026
04e894b
Merge pull request #1107 from aerele/backport/1102-desk-nudge-cta-ver…
kavin-114 Sep 4, 2026
697b579
Merge pull request #1108 from aerele/backport/1103-support-count-pill…
kavin-114 Sep 4, 2026
7e07be2
test(backport): inline the listing access grant, the #1062 helper is …
kavin-114 Sep 4, 2026
cf22009
Merge pull request #1110 from aerele/backport/1104-schedule-day-picke…
kavin-114 Sep 4, 2026
7ea0685
Merge pull request #1111 from aerele/fix/force-resync-sidecars
kavin-114 Sep 4, 2026
08e2edd
Merge pull request #1112 from aerele/backport/1111-force-resync-sidec…
kavin-114 Sep 4, 2026
f22a254
feat(onboarding): billing details for GST invoicing (Company + Invoic…
Navin-S-R Sep 4, 2026
c91a64a
feat(usage): schema_version 2 rollup - per-user context + users_daily…
kavin-114 Sep 4, 2026
8be5e32
fix(usage): review fixes for PR #1124 (context snapshot correctness)
kavin-114 Sep 4, 2026
264e827
fix(usage): refresh_session_snapshots must not fabricate last_usage_at
kavin-114 Sep 4, 2026
41e0520
fix(chat): never refuse a send on a stale RQ worker registry (backpor…
kavin-114 Sep 4, 2026
6754f0d
Merge pull request #1120 from aerele/mergify/bp/version-15/pr-1080
venkat102 Sep 4, 2026
38c69a6
Merge pull request #1125 from aerele/mergify/bp/version-15-hotfix/pr-…
venkat102 Sep 4, 2026
5c1f6cb
Merge pull request #1128 from aerele/mergify/bp/version-15-hotfix/pr-…
venkat102 Sep 4, 2026
ccc5ecd
feat(billing): customer billing portal — invoices + editable billing …
Navin-S-R Sep 2, 2026
1ba1581
feat(billing): drop the partner "billed through" branch (always bill …
Navin-S-R Sep 3, 2026
4bbc19b
feat(billing): Invoicing Details party in the portal card
Navin-S-R Sep 3, 2026
1a93198
fix(billing): type-annotate update_billing_details' billing param
Navin-S-R Sep 4, 2026
3a71f03
fix(onboarding): capture a valid billing country + close address comp…
Navin-S-R Sep 4, 2026
ecc813c
fix(onboarding): validate + canonicalise a restored/defaulted country…
Navin-S-R Sep 4, 2026
da36869
Merge pull request #1132 from aerele/mergify/bp/version-15-hotfix/pr-…
Navin-S-R Sep 5, 2026
1f8f386
Merge pull request #1134 from aerele/mergify/bp/version-15-hotfix/pr-…
Navin-S-R Sep 5, 2026
a32127f
feat(onboarding): validate State ↔ Pincode ↔ GSTIN address consistency
Navin-S-R Sep 5, 2026
8e84a29
Merge pull request #1138 from aerele/mergify/bp/version-15-hotfix/pr-…
Navin-S-R Sep 5, 2026
b6c8ad8
Merge pull request #1136 from aerele/feat/chat-context-meter
kavin-114 Sep 5, 2026
e2a7889
Merge pull request #1143 from aerele/backport/1136-version-15
kavin-114 Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

## Pre-merge checklist

> ℹ️ These repos are private on the GitHub **Free** plan, so branch protection is
> **not enforced** — CI cannot hard-block a merge. Honoring this checklist is what keeps
> broken changes out of UAT. See [`CONTRIBUTING.md`](../CONTRIBUTING.md).
> ℹ️ A red check only blocks the merge where the branch ruleset lists it as required.
> Honoring this checklist is what keeps broken changes out of UAT. See
> [`CONTRIBUTING.md`](../CONTRIBUTING.md).

- [ ] **CI is green** — the `tests` check on this PR passes (never merge on ❌)
- [ ] Branch is **up to date with `main`** (so it is tested against the latest code)
- [ ] Branch is **up to date with its base** (`develop`, or `version-N-hotfix` for a backport)
- [ ] New/changed behavior has tests (the coverage gate still passes)
- [ ] I self-reviewed the diff
- [ ] If the base is `version-N`: this is the release PR from `version-N-hotfix`, `__version__` is bumped, and `release-source` is green
39 changes: 31 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,23 @@ name: CI

on:
workflow_dispatch:
# Default PR types only, deliberately without `edited` (the event a base retarget sends).
# Listening to it and skipping the jobs on title edits was tried and is a trap: a skipped
# job still posts a "skipped" check run, GitHub reads the LATEST check per name, and a
# skipped check satisfies a required check, so a title edit after a red run would mask
# the failure. release-guard.yml instead fails a PR whose base changed until it is pushed
# to or reopened, which is what makes this workflow run against the new base.
pull_request:
push:
branches: [version-15]
# version-15 is the stable line customers install from; version-15-hotfix is where its
# backports collect until a release merges them into it. See CONTRIBUTING.md.
branches: [version-15, version-15-hotfix]

concurrency:
group: ci-${{ github.ref }}
# Never auto-cancel a version-15 run (cancelled reads as neutral, not failed → hides
# regressions on the release branch).
cancel-in-progress: ${{ github.ref != 'refs/heads/version-15' }}
# Only auto-cancel PR runs. A cancelled run on a long-lived branch reads as neutral, not
# failed, which hides regressions on the release line.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
Expand All @@ -64,8 +72,8 @@ jobs:
- uses: pre-commit/action@v3.0.1
env:
# no-commit-to-branch is a LOCAL guard: it inspects the checked-out
# branch name. On `push: branches: [version-15]` the checkout IS
# version-15, so leaving it on would fail this job on every merge. It
# branch name. On a `push` event the checkout IS the guarded branch,
# so leaving it on would fail this job on every merge. It
# has no meaning server-side anyway - the PR requirement is what
# enforces it.
SKIP: no-commit-to-branch
Expand Down Expand Up @@ -124,10 +132,25 @@ jobs:
- name: node:test (Desk widget + PWA lib)
env:
NODE_OPTIONS: --unhandled-rejections=strict
# These two suites are plain node:test with only sibling-module imports (no npm deps),
# so they run from the repo root with bare node - no `npm ci` needed.
# These suites are plain node:test with only sibling-module imports (no npm deps),
# so they run from the repo root with bare node - no `npm ci` needed. They were run by
# NOTHING in CI before, which is exactly how a Desk-widget confirmation-card ordering
# bug (a dropped field that renumbered a typed "confirm N" onto the wrong ERP write)
# shipped under a green build. Gate them here.
#
# Each glob is `dir/*.test.mjs` - NOT recursive - so it covers only *.test.mjs files
# sitting directly in that one directory, never a subdirectory's. jarvis/public/js/
# has two: jarvis_onboarding_banner.test.mjs directly in it (the same gap as the
# widget's, for the Desk nudge bundle - it `require()`s the bundle's CommonJS export
# guard rather than a plain sibling import, see that file's own comment) and
# pump_fence.test.mjs under shared/, which needs its own explicit pattern for exactly
# this reason - it went ungated for a while after jarvis/public/js/*.test.mjs was
# added, on the same silent-mismatch shape this whole job exists to close. A NEW
# subdirectory under jarvis/public/js/ with its own *.test.mjs needs the same: add its
# own line/pattern here, do not assume an existing glob already reaches it.
run: |
node --test jarvis/public/js/jarvis_chat/widget/*.test.mjs
node --test jarvis/public/js/*.test.mjs jarvis/public/js/shared/*.test.mjs
node --test pwa/src/lib/*.test.js

# Builds BOTH deploy artifacts (the /jarvis SPA and the /jarvis-mobile PWA) on Node
Expand Down
98 changes: 98 additions & 0 deletions .github/workflows/release-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Release guard

# Stable branches (version-N) accept exactly one kind of PR: the release PR from their own
# hotfix branch (version-N-hotfix). Backports target the hotfix branch; a release is the
# merge of hotfix into stable. This job fails any other PR into a stable branch, and on a
# release PR it also fails unless __version__ moved up and matches the line. See
# CONTRIBUTING.md.
#
# Enforcement is a repo-settings matter, not this file's: the job only blocks a merge once
# `release-source` is listed as a required status check on the version-* rulesets. Until
# then it is advisory, a red check a reviewer has to notice.
#
# Two deliberate choices:
# * pull_request_target, not pull_request: the workflow then comes from the BASE branch's
# copy of this file, so a head branch that predates it, or a fork that deletes it, does
# not silently produce "no check". Safe here because nothing is checked out and no
# head code runs; the job only reads event fields and, for a release PR, fetches one
# file through the API.
# * Its own workflow, not a job in ci.yml: `edited` is the event GitHub sends when a
# PR's BASE is changed. Retargeting a green PR from develop to version-16 must re-run
# this check, but re-running the whole test matrix on every title edit would be waste.

on:
pull_request_target:
types: [opened, synchronize, reopened, edited]

permissions:
contents: read

# Repeated `edited` events (title tweaks, a double retarget) supersede each other.
concurrency:
group: release-guard-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
release-source:
runs-on: ubuntu-latest
timeout-minutes: 2
env:
BASE: ${{ github.base_ref }}
HEAD: ${{ github.head_ref }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
THIS_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
steps:
- name: A retargeted PR must be pushed to or reopened before it can merge
# ci.yml does not run on `edited` (see the note there), so after a base change its
# green checks describe the OLD base. Fail here until a push or a close/reopen
# makes CI run against the new one; both events re-run this job too.
if: github.event.action == 'edited' && github.event.changes.base != null
run: |
echo "::error::Base changed from ${{ github.event.changes.base.ref.from }} to $BASE. CI has not run against the new base: push a commit or close and reopen this PR."
exit 1

- name: Only version-N-hotfix (same repo) may open a PR into version-N
run: |
case "$BASE" in
version-[0-9]*-hotfix) echo "hotfix base: any head is allowed"; exit 0 ;;
version-[0-9]*) ;;
*) echo "not a stable base: nothing to check"; exit 0 ;;
esac
if [ "$HEAD_REPO" != "$THIS_REPO" ]; then
echo "::error::PRs into $BASE must come from a branch in $THIS_REPO, not a fork (got $HEAD_REPO:$HEAD)."
exit 1
fi
if [ "$HEAD" != "${BASE}-hotfix" ]; then
echo "::error::PRs into $BASE must come from ${BASE}-hotfix (got $HEAD). Retarget this PR to ${BASE}-hotfix; a release is cut by merging ${BASE}-hotfix into $BASE."
exit 1
fi
echo "release PR: $HEAD -> $BASE"

- name: A release PR must raise __version__ and stay on its line
# Runs only for the release-PR shape (a non-release PR into a stable branch already
# failed above; a non-stable base skips). A release that ships without the bump
# leaves every bench on it stuck behind the control plane's release notice, with
# nothing to dismiss it. Read through the API: nothing is checked out here.
if: startsWith(github.base_ref, 'version-') && !endsWith(github.base_ref, '-hotfix')
run: |
read_version() {
gh api "repos/$THIS_REPO/contents/jarvis/__init__.py?ref=$1" --jq .content \
| base64 -d | sed -n 's/^__version__ = "\([^"]*\)".*/\1/p'
}
base_v="$(read_version "$BASE_SHA")"
head_v="$(read_version "$HEAD_SHA")"
line="${BASE#version-}"
echo "base $base_v -> head $head_v (line $line)"
if [ -z "$head_v" ]; then
echo "::error::could not read __version__ from jarvis/__init__.py on $HEAD"; exit 1
fi
if [ "${head_v%%.*}" != "$line" ]; then
echo "::error::__version__ $head_v is not on the $line.x line of $BASE"; exit 1
fi
if [ "$(printf '%s\n%s\n' "$base_v" "$head_v" | sort -V | tail -1)" != "$head_v" ] || [ "$base_v" = "$head_v" ]; then
echo "::error::__version__ must move up on a release PR: $BASE has $base_v, $HEAD has $head_v. Bump jarvis/__init__.py on $HEAD first."; exit 1
fi
echo "version bump ok"
72 changes: 72 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Release

# Tags and publishes a release whenever a stable branch (version-N) moves. Under the hotfix
# model the only thing that lands on a stable branch is the release PR from version-N-hotfix,
# and that PR carries the __version__ bump (release-guard.yml refuses it otherwise), so every
# push here IS a release: read __version__, tag the merge commit vN.x.y, publish a GitHub
# Release with notes generated from the previous tag on the same line.
#
# Idempotent: if the tag already exists nothing happens, so a failed run can be re-run from
# the Actions tab (workflow_dispatch) without side effects. "Latest" goes only to the highest
# version-N line, so a v15 release never displaces the v16 one.
#
# The token is github-actions[bot]. The "Release tags" ruleset therefore must NOT carry a
# tag `creation` rule (GitHub refuses the built-in Actions app as a bypass actor); it keeps
# `deletion`, `update` and the vN.N.N name pattern, which is what makes a published tag
# immutable.

on:
push:
branches: [version-15, version-16]
workflow_dispatch:

permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
tag-and-release:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Tag and publish if __version__ is new
run: |
set -euo pipefail
v="$(sed -n 's/^__version__ = "\([^"]*\)".*/\1/p' jarvis/__init__.py)"
if [[ ! "$GITHUB_REF_NAME" =~ ^version-[0-9]+$ ]]; then
echo "::error::Release runs only on a version-N branch (got $GITHUB_REF_NAME)"; exit 1
fi
line="${GITHUB_REF_NAME#version-}"
if [ -z "$v" ] || [ "${v%%.*}" != "$line" ]; then
echo "::error::__version__ '$v' is not on the $line.x line of $GITHUB_REF_NAME"; exit 1
fi
# Tag and Release are checked separately so a re-run finishes whatever step failed:
# a pushed tag with no Release (API hiccup) must still get its Release.
if git rev-parse -q --verify "refs/tags/v$v" >/dev/null; then
echo "tag v$v exists"
else
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "v$v" -m "v$v" "$GITHUB_SHA"
git push origin "v$v"
echo "tagged v$v at $GITHUB_SHA"
fi
if gh release view "v$v" >/dev/null 2>&1; then
echo "release v$v exists, nothing to do"; exit 0
fi
prev="$(git tag -l "v$line.*" --sort=-v:refname | grep -vx "v$v" | head -1 || true)"
highest="$(git ls-remote --heads origin 'version-*' | sed -n 's|.*/version-\([0-9]*\)$|\1|p' | sort -n | tail -1)"
latest=false; [ "$line" = "$highest" ] && latest=true
echo "publishing v$v on $GITHUB_REF_NAME (previous: ${prev:-none}, latest=$latest)"
gh release create "v$v" --verify-tag --title "v$v" --generate-notes \
${prev:+--notes-start-tag "$prev"} --latest="$latest"
9 changes: 5 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ repos:
- id: trailing-whitespace
files: "jarvis.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
# Work lands on develop through a PR, never a direct commit. Branch
# protection is a paid feature on private repos, so this is the local
# equivalent - it holds on every machine that runs `pre-commit install`.
# Work lands on the five long-lived branches (develop, version-N,
# version-N-hotfix) through a PR, never a direct commit. The branch
# rulesets enforce that server-side; this is the local equivalent, and it
# holds on every machine that runs `pre-commit install`.
- id: no-commit-to-branch
args: ["--branch", "develop", "--branch", "beta"]
args: ["--branch", "develop", "--pattern", "^version-[0-9]+(-hotfix)?$"]
- id: check-merge-conflict
- id: check-ast
- id: check-json
Expand Down
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,31 @@ time.

If you find a problem while using Jarvis, please report it through **Support**
inside the app.

## Branches and releases

| Branch | Role | What may merge into it |
|---|---|---|
| `develop` | default; all work lands here first | feature and fix PRs |
| `version-16-hotfix`, `version-15-hotfix` | backports waiting for the next release | backport PRs (cherry-picks from `develop`), fixes found in production |
| `version-16`, `version-15` | stable; what customers install and what Frappe Cloud tracks | **only** the release PR from the matching hotfix branch |

- A backport PR targets `version-N-hotfix`, never `version-N`. The `release-source` check
(`.github/workflows/release-guard.yml`) fails any other PR into a stable branch. It
blocks the merge only where the branch ruleset lists it as a required check; wire that
in repo settings, the workflow cannot do it by itself.
- A fix found in production goes to the hotfix branch first, then is forward-ported to
`develop` in its own PR so it is not lost on the next backport.
- A release is one PR, `version-N-hotfix` -> `version-N`, titled `chore: release vN.x.y`,
merged with a merge commit. Before opening it, bump `__version__` in `jarvis/__init__.py`
on the hotfix branch (feature backports bump minor, fix-only bumps patch). On merge the
`Release` workflow (`.github/workflows/release.yml`) tags the merge commit `vN.x.y` and
publishes a GitHub Release with notes generated from the previous tag on that line.
Check the Releases page afterwards; if the run failed, re-run it from the Actions tab.
It resumes whatever step was missing (tag, Release, or nothing).
- Never push directly to any of these five branches; everything lands through a PR.
The `version-N` rulesets enforce this today, and the `version-N-hotfix` rulesets should
match them (PR required, no force-push, no deletion).
- On a release PR the same check also fails unless `__version__` moved up and its major
matches the line, so a release cannot ship without the bump.

21 changes: 21 additions & 0 deletions frontend/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export const setAutoApply = (conversation, value) =>
// null/zeros until then (design doc §6, UsagePane's "Measured usage" block).
export const getUsage = (conversation) =>
call("jarvis.chat.api.get_usage", { conversation: conversation || "" });
// Context-window meter for one chat: {used, capacity, pct, warn_pct,
// auto_compact_pct, route, compaction_count, last_compacted_at, compacting,
// fresh}. Bench snapshot, refreshed after every completed turn.
export const getConversationContext = (conversation) =>
call("jarvis.chat.api.get_conversation_context", { conversation });
// Summarise older turns. Optional hint = what to keep. Resolves to
// {ok, queued} or {ok:false, reason}; the result arrives later as a
// context:compacted / context:compact_failed event.
export const compactConversation = (conversation, hint = "") =>
call("jarvis.chat.api.compact_conversation", { conversation, hint: hint || null });
// Tool runs recorded in one chat, newest turn first, from the PERSISTED tool
// rows: the same rows the thread's Activity accordion renders. The Settings
// Activity pane used to derive this from the browser's live run stream, which
Expand Down Expand Up @@ -609,6 +619,17 @@ export const billingPaymentState = () => call("jarvis.account.get_billing_paymen
export const checkBillingPayment = (opts) =>
rawOnboardingCall("jarvis.account.check_billing_payment_status", {}, opts);

// GST invoices + billing details for the billing page (Phase 3b). getInvoices lists the
// customer's own invoices; downloadInvoice returns a base64 PDF (admin re-verifies ownership).
// getBillingProfile/updateBillingDetails drive the editable billing-details card (a partner-
// billed customer gets a read-only "billed through <partner>" and cannot edit).
export const getInvoices = () => call("jarvis.account.get_invoices");
export const downloadInvoice = (erpName) =>
call("jarvis.account.download_invoice", { erp_name: erpName });
export const getBillingProfile = () => call("jarvis.account.get_billing_profile");
export const updateBillingDetails = (billing) =>
call("jarvis.account.update_billing_details", { billing });

// File input: upload to Frappe's File doctype, return {file_url, file_name}.
export async function uploadFile(file) {
const fd = new FormData();
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/components/chat/CompactDialog.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect, vi } from "vitest";
import { mount } from "@vue/test-utils";

vi.mock("frappe-ui", () => ({
Dialog: {
props: ["modelValue", "options"],
template: "<div><slot name='body-content'/><slot name='actions'/></div>",
},
Button: {
props: ["label", "variant", "disabled", "loading"],
emits: ["click"],
template: "<button :disabled='disabled' @click=\"$emit('click')\">{{ label }}</button>",
},
FormControl: {
props: ["modelValue", "label", "placeholder", "type"],
emits: ["update:modelValue"],
template:
"<textarea :value='modelValue' @input=\"$emit('update:modelValue', $event.target.value)\"/>",
},
}));

import CompactDialog from "./CompactDialog.vue";

describe("CompactDialog", () => {
it("confirms with the trimmed hint", async () => {
const w = mount(CompactDialog, { props: { modelValue: true } });
await w.find("textarea").setValue(" keep the invoice inputs ");
await w.findAll("button").at(-1).trigger("click");
expect(w.emitted("confirm")[0]).toEqual(["keep the invoice inputs"]);
});
it("disables Compact with the busy reason", () => {
const w = mount(CompactDialog, {
props: { modelValue: true, busyReason: "A reply is in progress" },
});
expect(w.findAll("button").at(-1).attributes("disabled")).toBeDefined();
expect(w.text()).toContain("A reply is in progress");
});
});
Loading
Loading