summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-18 23:56:34 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-18 23:56:34 +0000
commit599a26d556df52b364b5b540762a521d22eb5b7b (patch)
tree740c141c52764604fc8d4c036733e5f47368b26a /web
parent0db05b0fa6de318f164a1d73ddc55db9c59f1fc3 (diff)
parent7df4f06ae0e3ae80bd967bf53cbec36e58b4a3bd (diff)
Merge feat/container-execution into master
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web')
-rw-r--r--web/app.js350
-rw-r--r--web/index.html13
-rw-r--r--web/test/tab-persistence.test.mjs58
-rw-r--r--web/test/task-panel-summary.test.mjs143
4 files changed, 361 insertions, 203 deletions
diff --git a/web/app.js b/web/app.js
index f785605..6ddb23c 100644
--- a/web/app.js
+++ b/web/app.js
@@ -407,6 +407,14 @@ export function setTaskFilterTab(tab) {
localStorage.setItem('taskFilterTab', tab);
}
+export function getActiveMainTab() {
+ return localStorage.getItem('activeMainTab') ?? 'queue';
+}
+
+export function setActiveMainTab(tab) {
+ localStorage.setItem('activeMainTab', tab);
+}
+
// ── Tab badge counts ───────────────────────────────────────────────────────────
/**
@@ -1470,12 +1478,13 @@ function buildValidatePayload() {
const f = document.getElementById('task-form');
const name = f.querySelector('[name="name"]').value;
const instructions = f.querySelector('[name="instructions"]').value;
- const project_dir = f.querySelector('#project-select').value;
+ const repository_url = document.getElementById('repository-url').value;
+ const container_image = document.getElementById('container-image').value;
const allowedToolsEl = f.querySelector('[name="allowed_tools"]');
const allowed_tools = allowedToolsEl
? allowedToolsEl.value.split(',').map(s => s.trim()).filter(Boolean)
: [];
- return { name, agent: { instructions, project_dir, allowed_tools } };
+ return { name, repository_url, agent: { instructions, container_image, allowed_tools } };
}
function renderValidationResult(result) {
@@ -1533,49 +1542,6 @@ function renderValidationResult(result) {
async function openTaskModal() {
document.getElementById('task-modal').showModal();
- await populateProjectSelect();
-}
-
-async function populateProjectSelect() {
- const select = document.getElementById('project-select');
- const current = select.value;
- try {
- const res = await fetch(`${API_BASE}/api/workspaces`);
- const dirs = await res.json();
- select.innerHTML = '';
- dirs.forEach(dir => {
- const opt = document.createElement('option');
- opt.value = dir;
- opt.textContent = dir;
- if (dir === current || dir === '/workspace/claudomator') opt.selected = true;
- select.appendChild(opt);
- });
- } catch {
- // keep whatever options are already there
- }
- // Ensure "Create new project…" option is always last
- const newOpt = document.createElement('option');
- newOpt.value = '__new__';
- newOpt.textContent = 'Create new project…';
- select.appendChild(newOpt);
-}
-
-function initProjectSelect() {
- const select = document.getElementById('project-select');
- const newRow = document.getElementById('new-project-row');
- const newInput = document.getElementById('new-project-input');
- if (!select) return;
- select.addEventListener('change', () => {
- if (select.value === '__new__') {
- newRow.hidden = false;
- newInput.required = true;
- newInput.focus();
- } else {
- newRow.hidden = true;
- newInput.required = false;
- newInput.value = '';
- }
- });
}
function closeTaskModal() {
@@ -1589,20 +1555,20 @@ function closeTaskModal() {
}
async function createTask(formData) {
- const selectVal = formData.get('project_dir');
- const workingDir = selectVal === '__new__'
- ? document.getElementById('new-project-input').value.trim()
- : selectVal;
+ const repository_url = formData.get('repository_url');
+ const container_image = formData.get('container_image');
const elaboratePromptEl = document.getElementById('elaborate-prompt');
const elaborationInput = elaboratePromptEl ? elaboratePromptEl.value.trim() : '';
const body = {
name: formData.get('name'),
description: '',
elaboration_input: elaborationInput || undefined,
+ repository_url: repository_url,
agent: {
instructions: formData.get('instructions'),
- project_dir: workingDir,
+ container_image: container_image,
max_budget_usd: parseFloat(formData.get('max_budget_usd')),
+ type: 'container',
},
timeout: formData.get('timeout'),
priority: formData.get('priority'),
@@ -1743,7 +1709,7 @@ function makeMetaItem(label, valueText, opts = {}) {
return item;
}
-function renderTaskPanel(task, executions) {
+export function renderTaskPanel(task, executions) {
document.getElementById('task-panel-title').textContent = task.name;
const content = document.getElementById('task-panel-content');
content.innerHTML = '';
@@ -2729,6 +2695,8 @@ async function renderDropsPanel() {
// ── Tab switching ─────────────────────────────────────────────────────────────
function switchTab(name) {
+ setActiveMainTab(name);
+
// Update tab button active state
document.querySelectorAll('.tab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === name);
@@ -2749,169 +2717,161 @@ function switchTab(name) {
// ── Boot ──────────────────────────────────────────────────────────────────────
-if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded', () => {
- document.getElementById('btn-start-next').addEventListener('click', function() {
- handleStartNextTask(this);
- });
-
- switchTab('queue');
- startPolling();
- connectWebSocket();
+if (typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', () => {
+ document.getElementById('btn-start-next').addEventListener('click', function() {
+ handleStartNextTask(this);
+ });
- // Side panel close
- document.getElementById('btn-close-panel').addEventListener('click', closeTaskPanel);
- document.getElementById('task-panel-backdrop').addEventListener('click', closeTaskPanel);
+ switchTab(getActiveMainTab());
+ startPolling();
+ connectWebSocket();
- // Execution logs modal close
- document.getElementById('btn-close-logs').addEventListener('click', () => {
- document.getElementById('logs-modal').close();
- });
+ // Side panel close
+ document.getElementById('btn-close-panel').addEventListener('click', closeTaskPanel);
+ document.getElementById('task-panel-backdrop').addEventListener('click', closeTaskPanel);
- // Tab bar
- document.querySelectorAll('.tab').forEach(btn => {
- btn.addEventListener('click', () => switchTab(btn.dataset.tab));
- });
+ // Execution logs modal close
+ document.getElementById('btn-close-logs').addEventListener('click', () => {
+ document.getElementById('logs-modal').close();
+ });
- // Task modal
- document.getElementById('btn-new-task').addEventListener('click', openTaskModal);
- document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal);
- initProjectSelect();
+ // Tab bar
+ document.querySelectorAll('.tab').forEach(btn => {
+ btn.addEventListener('click', () => switchTab(btn.dataset.tab));
+ });
- // Push notifications button
- const btnNotify = document.getElementById('btn-notifications');
- if (btnNotify) {
- btnNotify.addEventListener('click', () => enableNotifications(btnNotify));
- }
+ // Task modal
+ document.getElementById('btn-new-task').addEventListener('click', openTaskModal);
+ document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal);
- // Validate button
- document.getElementById('btn-validate').addEventListener('click', async () => {
- const btn = document.getElementById('btn-validate');
- const resultDiv = document.getElementById('validate-result');
- btn.disabled = true;
- btn.textContent = 'Checking…';
- try {
- const payload = buildValidatePayload();
- const result = await validateTask(payload);
- renderValidationResult(result);
- } catch (err) {
- resultDiv.removeAttribute('hidden');
- resultDiv.textContent = 'Validation failed: ' + err.message;
- } finally {
- btn.disabled = false;
- btn.textContent = 'Validate Instructions';
+ // Push notifications button
+ const btnNotify = document.getElementById('btn-notifications');
+ if (btnNotify) {
+ btnNotify.addEventListener('click', () => enableNotifications(btnNotify));
}
- });
- // Draft with AI button
- const btnElaborate = document.getElementById('btn-elaborate');
- btnElaborate.addEventListener('click', async () => {
- const prompt = document.getElementById('elaborate-prompt').value.trim();
- if (!prompt) {
- const form = document.getElementById('task-form');
- // Remove previous error
- const prev = form.querySelector('.form-error');
- if (prev) prev.remove();
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = 'Please enter a description before drafting.';
- form.querySelector('.elaborate-section').appendChild(errEl);
- return;
- }
+ // Validate button
+ document.getElementById('btn-validate').addEventListener('click', async () => {
+ const btn = document.getElementById('btn-validate');
+ const resultDiv = document.getElementById('validate-result');
+ btn.disabled = true;
+ btn.textContent = 'Checking…';
+ try {
+ const payload = buildValidatePayload();
+ const result = await validateTask(payload);
+ renderValidationResult(result);
+ } catch (err) {
+ resultDiv.removeAttribute('hidden');
+ resultDiv.textContent = 'Validation failed: ' + err.message;
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Validate Instructions';
+ }
+ });
+
+ // Draft with AI button
+ const btnElaborate = document.getElementById('btn-elaborate');
+ btnElaborate.addEventListener('click', async () => {
+ const prompt = document.getElementById('elaborate-prompt').value.trim();
+ if (!prompt) {
+ const form = document.getElementById('task-form');
+ // Remove previous error
+ const prev = form.querySelector('.form-error');
+ if (prev) prev.remove();
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = 'Please enter a description before drafting.';
+ form.querySelector('.elaborate-section').appendChild(errEl);
+ return;
+ }
- btnElaborate.disabled = true;
- btnElaborate.textContent = 'Drafting…';
+ btnElaborate.disabled = true;
+ btnElaborate.textContent = 'Drafting…';
- // Remove any previous errors or banners
- const form = document.getElementById('task-form');
- form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove());
+ // Remove any previous errors or banners
+ const form = document.getElementById('task-form');
+ form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove());
- try {
- const sel = document.getElementById('project-select');
- const workingDir = sel.value === '__new__'
- ? document.getElementById('new-project-input').value.trim()
- : sel.value;
- const result = await elaborateTask(prompt, workingDir);
-
- // Populate form fields
- const f = document.getElementById('task-form');
- if (result.name)
- f.querySelector('[name="name"]').value = result.name;
- if (result.agent && result.agent.instructions)
- f.querySelector('[name="instructions"]').value = result.agent.instructions;
- if (result.agent && (result.agent.project_dir || result.agent.working_dir)) {
- const pDir = result.agent.project_dir || result.agent.working_dir;
- const pSel = document.getElementById('project-select');
- const exists = [...pSel.options].some(o => o.value === pDir);
- if (exists) {
- pSel.value = pDir;
- } else {
- pSel.value = '__new__';
- document.getElementById('new-project-row').hidden = false;
- document.getElementById('new-project-input').value = pDir;
+ try {
+ const repoUrl = document.getElementById('repository-url').value.trim();
+ const result = await elaborateTask(prompt, repoUrl);
+
+ // Populate form fields
+ const f = document.getElementById('task-form');
+ if (result.name)
+ f.querySelector('[name="name"]').value = result.name;
+ if (result.agent && result.agent.instructions)
+ f.querySelector('[name="instructions"]').value = result.agent.instructions;
+ if (result.repository_url || result.agent?.repository_url) {
+ document.getElementById('repository-url').value = result.repository_url || result.agent.repository_url;
}
- }
- if (result.agent && result.agent.max_budget_usd != null)
- f.querySelector('[name="max_budget_usd"]').value = result.agent.max_budget_usd;
- if (result.timeout)
- f.querySelector('[name="timeout"]').value = result.timeout;
- if (result.priority) {
- const sel = f.querySelector('[name="priority"]');
- if ([...sel.options].some(o => o.value === result.priority)) {
- sel.value = result.priority;
+ if (result.agent && result.agent.container_image) {
+ document.getElementById('container-image').value = result.agent.container_image;
+ }
+ if (result.agent && result.agent.max_budget_usd != null)
+ f.querySelector('[name="max_budget_usd"]').value = result.agent.max_budget_usd;
+ if (result.timeout)
+ f.querySelector('[name="timeout"]').value = result.timeout;
+ if (result.priority) {
+ const sel = f.querySelector('[name="priority"]');
+ if ([...sel.options].some(o => o.value === result.priority)) {
+ sel.value = result.priority;
+ }
}
- }
-
- // Show success banner
- const banner = document.createElement('p');
- banner.className = 'elaborate-banner';
- banner.textContent = 'AI draft ready — review and submit.';
- document.getElementById('task-form').querySelector('.elaborate-section').appendChild(banner);
- // Auto-validate after elaboration
- try {
- const result = await validateTask(buildValidatePayload());
- renderValidationResult(result);
- } catch (_) {
- // silent - elaboration already succeeded, validation is bonus
+ // Show success banner
+ const banner = document.createElement('p');
+ banner.className = 'elaborate-banner';
+ banner.textContent = 'AI draft ready — review and submit.';
+ document.getElementById('task-form').querySelector('.elaborate-section').appendChild(banner);
+
+ // Auto-validate after elaboration
+ try {
+ const result = await validateTask(buildValidatePayload());
+ renderValidationResult(result);
+ } catch (_) {
+ // silent - elaboration already succeeded, validation is bonus
+ }
+ } catch (err) {
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = `Elaboration failed: ${err.message}`;
+ document.getElementById('task-form').querySelector('.elaborate-section').appendChild(errEl);
+ } finally {
+ btnElaborate.disabled = false;
+ btnElaborate.textContent = 'Draft with AI ✦';
}
- } catch (err) {
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = `Elaboration failed: ${err.message}`;
- document.getElementById('task-form').querySelector('.elaborate-section').appendChild(errEl);
- } finally {
- btnElaborate.disabled = false;
- btnElaborate.textContent = 'Draft with AI ✦';
- }
- });
+ });
- document.getElementById('task-form').addEventListener('submit', async e => {
- e.preventDefault();
+ document.getElementById('task-form').addEventListener('submit', async e => {
+ e.preventDefault();
- // Remove any previous error
- const prev = e.target.querySelector('.form-error');
- if (prev) prev.remove();
+ // Remove any previous error
+ const prev = e.target.querySelector('.form-error');
+ if (prev) prev.remove();
- const btn = e.submitter;
- btn.disabled = true;
- btn.textContent = 'Creating…';
+ const btn = e.submitter;
+ btn.disabled = true;
+ btn.textContent = 'Creating…';
- try {
- const validateResult = document.getElementById('validate-result');
- if (!validateResult.hasAttribute('hidden') && validateResult.dataset.clarity && validateResult.dataset.clarity !== 'clear') {
- if (!window.confirm('The validator flagged issues. Create task anyway?')) {
- return;
+ try {
+ const validateResult = document.getElementById('validate-result');
+ if (!validateResult.hasAttribute('hidden') && validateResult.dataset.clarity && validateResult.dataset.clarity !== 'clear') {
+ if (!window.confirm('The validator flagged issues. Create task anyway?')) {
+ return;
+ }
}
+ await createTask(new FormData(e.target));
+ } catch (err) {
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = err.message;
+ e.target.appendChild(errEl);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Create & Queue';
}
- await createTask(new FormData(e.target));
- } catch (err) {
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = err.message;
- e.target.appendChild(errEl);
- } finally {
- btn.disabled = false;
- btn.textContent = 'Create & Queue';
- }
+ });
});
-});
+}
diff --git a/web/index.html b/web/index.html
index c17601b..7d52458 100644
--- a/web/index.html
+++ b/web/index.html
@@ -74,15 +74,12 @@
<p class="elaborate-hint">AI will fill in the form fields below. You can edit before submitting.</p>
</div>
<hr class="form-divider">
- <label>Project
- <select name="project_dir" id="project-select">
- <option value="/workspace/claudomator" selected>/workspace/claudomator</option>
- <option value="__new__">Create new project…</option>
- </select>
+ <label>Repository URL
+ <input name="repository_url" id="repository-url" placeholder="https://github.com/user/repo.git" required>
+ </label>
+ <label>Container Image
+ <input name="container_image" id="container-image" placeholder="claudomator-agent:latest" value="claudomator-agent:latest">
</label>
- <div id="new-project-row" hidden>
- <label>New Project Path <input id="new-project-input" placeholder="/workspace/my-new-app"></label>
- </div>
<label>Name <input name="name" required></label>
<label>Instructions <textarea name="instructions" rows="6" required></textarea></label>
<div class="validate-section">
diff --git a/web/test/tab-persistence.test.mjs b/web/test/tab-persistence.test.mjs
new file mode 100644
index 0000000..9311453
--- /dev/null
+++ b/web/test/tab-persistence.test.mjs
@@ -0,0 +1,58 @@
+// tab-persistence.test.mjs — TDD tests for main-tab localStorage persistence
+//
+// Run with: node --test web/test/tab-persistence.test.mjs
+
+import { describe, it, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
+
+// ── localStorage mock ──────────────────────────────────────────────────────────
+// Must be set up before importing app.js so the module sees the global.
+const store = new Map();
+globalThis.localStorage = {
+ getItem: (k) => store.has(k) ? store.get(k) : null,
+ setItem: (k, v) => store.set(k, String(v)),
+ removeItem: (k) => store.delete(k),
+ clear: () => store.clear(),
+};
+
+import { getActiveMainTab, setActiveMainTab } from '../app.js';
+
+describe('getActiveMainTab', () => {
+ beforeEach(() => store.clear());
+
+ it('returns "queue" when localStorage has no stored value', () => {
+ assert.equal(getActiveMainTab(), 'queue');
+ });
+
+ it('returns the tab name stored by setActiveMainTab', () => {
+ setActiveMainTab('settings');
+ assert.equal(getActiveMainTab(), 'settings');
+ });
+
+ it('returns "queue" after localStorage value is removed', () => {
+ setActiveMainTab('stats');
+ localStorage.removeItem('activeMainTab');
+ assert.equal(getActiveMainTab(), 'queue');
+ });
+
+ it('reflects the most recent setActiveMainTab call', () => {
+ setActiveMainTab('stats');
+ setActiveMainTab('running');
+ assert.equal(getActiveMainTab(), 'running');
+ });
+});
+
+describe('setActiveMainTab', () => {
+ beforeEach(() => store.clear());
+
+ it('writes the tab name to localStorage under key "activeMainTab"', () => {
+ setActiveMainTab('drops');
+ assert.equal(localStorage.getItem('activeMainTab'), 'drops');
+ });
+
+ it('overwrites a previously stored tab', () => {
+ setActiveMainTab('queue');
+ setActiveMainTab('interrupted');
+ assert.equal(localStorage.getItem('activeMainTab'), 'interrupted');
+ });
+});
diff --git a/web/test/task-panel-summary.test.mjs b/web/test/task-panel-summary.test.mjs
new file mode 100644
index 0000000..63dd483
--- /dev/null
+++ b/web/test/task-panel-summary.test.mjs
@@ -0,0 +1,143 @@
+// task-panel-summary.test.mjs — verifies task summary renders exactly once in panel.
+//
+// Run with: node --test web/test/task-panel-summary.test.mjs
+
+import { describe, it, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
+
+// ── Minimal DOM mock ──────────────────────────────────────────────────────────
+
+function makeMockDOM() {
+ const elements = {};
+
+ function createElement(tag) {
+ const el = {
+ tag,
+ className: '',
+ textContent: '',
+ innerHTML: '',
+ hidden: false,
+ children: [],
+ _listeners: {},
+ appendChild(child) { this.children.push(child); return child; },
+ prepend(...nodes) { this.children.unshift(...nodes); },
+ append(...nodes) { nodes.forEach(n => this.children.push(n)); },
+ querySelector(sel) {
+ const cls = sel.startsWith('.') ? sel.slice(1) : null;
+ function search(el) {
+ if (cls && el.className && el.className.split(' ').includes(cls)) return el;
+ for (const c of el.children || []) {
+ const found = search(c);
+ if (found) return found;
+ }
+ return null;
+ }
+ return search(this);
+ },
+ querySelectorAll(sel) {
+ const cls = sel.startsWith('.') ? sel.slice(1) : null;
+ const results = [];
+ function search(el) {
+ if (cls && el.className && el.className.split(' ').includes(cls)) results.push(el);
+ for (const c of el.children || []) search(c);
+ }
+ search(this);
+ return results;
+ },
+ addEventListener(ev, fn) {},
+ };
+ return el;
+ }
+
+ // Named panel elements referenced by getElementById
+ const panelTitle = createElement('h2');
+ const panelContent = createElement('div');
+ elements['task-panel-title'] = panelTitle;
+ elements['task-panel-content'] = panelContent;
+
+ const doc = {
+ createElement,
+ getElementById(id) { return elements[id] || null; },
+ };
+
+ return { doc, panelContent };
+}
+
+// ── Import renderTaskPanel ────────────────────────────────────────────────────
+
+import { renderTaskPanel } from '../app.js';
+
+// ── Tests ─────────────────────────────────────────────────────────────────────
+
+describe('renderTaskPanel summary rendering', () => {
+ it('renders task summary exactly once for a COMPLETED task', () => {
+ const { doc, panelContent } = makeMockDOM();
+
+ // Must set global document before calling renderTaskPanel
+ global.document = doc;
+
+ const task = {
+ id: 'task-1',
+ name: 'Fix the bug',
+ state: 'COMPLETED',
+ summary: 'Resolved the nil pointer in the payment handler.',
+ priority: 'normal',
+ created_at: '2026-03-17T10:00:00Z',
+ updated_at: '2026-03-17T10:05:00Z',
+ tags: [],
+ };
+
+ renderTaskPanel(task, []);
+
+ // Count all elements with class 'task-summary' or 'task-summary-text'
+ const summaryEls = panelContent.querySelectorAll('.task-summary');
+ const summaryTextEls = panelContent.querySelectorAll('.task-summary-text');
+
+ const total = summaryEls.length + summaryTextEls.length;
+ assert.equal(total, 1, `Expected exactly 1 summary element, got ${total} (task-summary: ${summaryEls.length}, task-summary-text: ${summaryTextEls.length})`);
+ });
+
+ it('uses task-summary class (not task-summary-text) for good contrast', () => {
+ const { doc, panelContent } = makeMockDOM();
+ global.document = doc;
+
+ const task = {
+ id: 'task-2',
+ name: 'Another task',
+ state: 'COMPLETED',
+ summary: 'All done.',
+ priority: 'high',
+ created_at: '2026-03-17T10:00:00Z',
+ updated_at: '2026-03-17T10:05:00Z',
+ tags: [],
+ };
+
+ renderTaskPanel(task, []);
+
+ const summaryEls = panelContent.querySelectorAll('.task-summary');
+ assert.equal(summaryEls.length, 1, 'Expected .task-summary element');
+ assert.equal(summaryEls[0].textContent, task.summary);
+ });
+
+ it('renders no summary section when task has no summary', () => {
+ const { doc, panelContent } = makeMockDOM();
+ global.document = doc;
+
+ const task = {
+ id: 'task-3',
+ name: 'Pending task',
+ state: 'PENDING',
+ summary: null,
+ priority: 'normal',
+ created_at: '2026-03-17T10:00:00Z',
+ updated_at: '2026-03-17T10:00:00Z',
+ tags: [],
+ };
+
+ renderTaskPanel(task, []);
+
+ const summaryEls = panelContent.querySelectorAll('.task-summary');
+ const summaryTextEls = panelContent.querySelectorAll('.task-summary-text');
+ assert.equal(summaryEls.length + summaryTextEls.length, 0, 'Expected no summary when task.summary is null');
+ });
+});