Apply new web plugin code layout to delete-project

And add basic karma testing capabilities.

Change-Id: Ib9742c79da68cc9426afd3d2947b9f6ce8be9631
diff --git a/BUILD b/BUILD
index 47d5db0..0dc7769 100644
--- a/BUILD
+++ b/BUILD
@@ -1,14 +1,11 @@
 load("@rules_java//java:defs.bzl", "java_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
-load("//tools/js:eslint.bzl", "eslint")
 load(
     "//tools/bzl:plugin.bzl",
     "PLUGIN_DEPS",
     "PLUGIN_TEST_DEPS",
     "gerrit_plugin",
 )
-load("//tools/bzl:js.bzl", "gerrit_js_bundle")
-load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
 
 gerrit_plugin(
     name = "delete-project",
@@ -19,39 +16,11 @@
         "Gerrit-HttpModule: com.googlesource.gerrit.plugins.deleteproject.HttpModule",
         "Gerrit-SshModule: com.googlesource.gerrit.plugins.deleteproject.SshModule",
     ],
-    resource_jars = [":gr-delete-repo"],
+    resource_jars = ["//plugins/delete-project/web:gr-delete-repo"],
     resources = glob(["src/main/resources/Documentation/*.md"]),
     deps = ["@commons-io//jar"],
 )
 
-ts_config(
-    name = "tsconfig",
-    src = "tsconfig.json",
-    deps = [
-        "//plugins:tsconfig-plugins-base.json",
-    ],
-)
-
-ts_project(
-    name = "gr-delete-repo-ts",
-    srcs = glob([
-        "gr-delete-repo/**/*.ts",
-    ]),
-    incremental = True,
-    tsc = "//tools/node_tools:tsc-bin",
-    tsconfig = ":tsconfig",
-    deps = [
-        "@plugins_npm//@gerritcodereview/typescript-api",
-        "@plugins_npm//lit",
-    ],
-)
-
-gerrit_js_bundle(
-    name = "gr-delete-repo",
-    srcs = [":gr-delete-repo-ts"],
-    entry_point = "gr-delete-repo/plugin.js",
-)
-
 junit_tests(
     name = "delete-project_tests",
     srcs = glob(["src/test/java/**/*.java"]),
@@ -71,28 +40,3 @@
         "@mockito//jar",
     ],
 )
-
-# The eslint macro creates 2 rules: lint_test and lint_bin. Typical usage:
-# bazel test $DIR:lint_test
-# bazel run $DIR:lint_bin -- --fix $PATH_TO_SRCS
-eslint(
-    name = "lint",
-    srcs = glob(["gr-delete-repo/**/*"]),
-    config = ".eslintrc.js",
-    data = [
-        "tsconfig.json",
-        "//plugins:.eslintrc.js",
-        "//plugins:.prettierrc.js",
-        "//plugins:tsconfig-plugins-base.json",
-    ],
-    extensions = [".ts"],
-    ignore = "//plugins:.eslintignore",
-    plugins = [
-        "@npm//eslint-config-google",
-        "@npm//eslint-plugin-html",
-        "@npm//eslint-plugin-import",
-        "@npm//eslint-plugin-jsdoc",
-        "@npm//eslint-plugin-prettier",
-        "@npm//gts",
-    ],
-)
diff --git a/README.md b/README.md
index 1444d1a..5a87b38 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,24 @@
 
 [![Build Status](https://gerrit-ci.gerritforge.com/view/Plugins-master/job/plugin-delete-project-bazel-master/badge/icon
 )](https://gerrit-ci.gerritforge.com/view/Plugins-master/job/plugin-delete-project-bazel-master/)
+
+## JavaScript Plugin Development
+
+For running unit tests execute:
+
+    bazel test --test_output=all //plugins/delete-project/web:karma_test
+
+For checking or fixing eslint formatter problems run:
+
+    bazel test //plugins/delete-project/web:lint_test
+    bazel run //plugins/delete-project/web:lint_bin -- --fix "$(pwd)/plugins/delete-project/web"
+
+For testing the plugin with
+[Gerrit FE Dev Helper](https://gerrit.googlesource.com/gerrit-fe-dev-helper/)
+build the JavaScript bundle and copy it to the `plugins/` folder:
+
+    bazel build //plugins/delete-project/web:gr-delete-repo
+    cp -f bazel-bin/plugins/delete-project/web/gr-delete-repo.js plugins/
+
+and let the Dev Helper redirect from `.+/plugins/delete-project/static/gr-delete-repo.js` to
+`http://localhost:8081/plugins_/gr-delete-repo.js`.
diff --git a/package.json b/package.json
deleted file mode 100644
index 5a46795..0000000
--- a/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "name": "delete-project",
-  "description": "Delete project plugin",
-  "browser": true,
-  "scripts": {
-    "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
-    "eslint": "npm run safe_bazelisk test :lint_test",
-    "eslintfix": "npm run safe_bazelisk run :lint_bin -- -- --fix $(pwd)"
-  },
-  "devDependencies": {},
-  "license": "Apache-2.0",
-  "private": true
-}
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 9be4292..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "extends": "../tsconfig-plugins-base.json",
-  "include": [
-    "gr-delete-repo/**/*",
-  ]
-}
diff --git a/.eslintrc.js b/web/.eslintrc.js
similarity index 89%
rename from .eslintrc.js
rename to web/.eslintrc.js
index 30df322..ee166d3 100644
--- a/.eslintrc.js
+++ b/web/.eslintrc.js
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-__plugindir = 'delete-project';
+__plugindir = 'delete-project/web';
 module.exports = {
-  extends: '../.eslintrc.js',
+  extends: '../../.eslintrc.js',
 };
diff --git a/web/BUILD b/web/BUILD
new file mode 100644
index 0000000..5490c23
--- /dev/null
+++ b/web/BUILD
@@ -0,0 +1,61 @@
+load("//tools/js:eslint.bzl", "plugin_eslint")
+load("//tools/bzl:js.bzl", "gerrit_js_bundle", "karma_test")
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+
+package_group(
+    name = "visibility",
+    packages = ["//plugins/delete-project/..."],
+)
+
+package(default_visibility = [":visibility"])
+
+ts_config(
+    name = "tsconfig",
+    src = "tsconfig.json",
+    deps = [
+        "//plugins:tsconfig-plugins-base.json",
+    ],
+)
+
+ts_project(
+    name = "delete-project-ts",
+    srcs = glob(
+        ["**/*.ts"],
+        exclude = ["**/*test*"],
+    ),
+    incremental = True,
+    out_dir = "_bazel_ts_out",
+    tsc = "//tools/node_tools:tsc-bin",
+    tsconfig = ":tsconfig",
+    deps = [
+        "@plugins_npm//@gerritcodereview/typescript-api",
+        "@plugins_npm//lit",
+    ],
+)
+
+ts_project(
+    name = "delete-project-ts-tests",
+    srcs = glob(["**/*.ts"]),
+    incremental = True,
+    out_dir = "_bazel_ts_out_tests",
+    tsc = "//tools/node_tools:tsc-bin",
+    tsconfig = ":tsconfig",
+    deps = [
+        "@plugins_npm//:node_modules",
+        "@ui_dev_npm//:node_modules",
+    ],
+)
+
+gerrit_js_bundle(
+    name = "gr-delete-repo",
+    srcs = [":delete-project-ts"],
+    entry_point = "_bazel_ts_out/plugin.js",
+)
+
+karma_test(
+    name = "karma_test",
+    srcs = ["karma_test.sh"],
+    data = [":delete-project-ts-tests"],
+)
+
+plugin_eslint()
diff --git a/gr-delete-repo/gr-delete-repo.ts b/web/gr-delete-repo.ts
similarity index 98%
rename from gr-delete-repo/gr-delete-repo.ts
rename to web/gr-delete-repo.ts
index e16cefb..dec8efe 100644
--- a/gr-delete-repo/gr-delete-repo.ts
+++ b/web/gr-delete-repo.ts
@@ -36,7 +36,7 @@
 }
 
 @customElement('gr-delete-repo')
-class GrDeleteRepo extends LitElement {
+export class GrDeleteRepo extends LitElement {
   @query('#deleteRepoOverlay')
   deleteRepoOverlay?: GrOverlay;
 
diff --git a/web/gr-delete-repo_test.ts b/web/gr-delete-repo_test.ts
new file mode 100644
index 0000000..2da6b2b
--- /dev/null
+++ b/web/gr-delete-repo_test.ts
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './test/test-setup';
+import './gr-delete-repo';
+import {queryAndAssert} from './test/test-util';
+import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
+import {GrDeleteRepo} from './gr-delete-repo';
+import {
+  ConfigInfo,
+  HttpMethod,
+  RepoName,
+} from '@gerritcodereview/typescript-api/rest-api';
+
+suite('gr-delete-repo tests', () => {
+  let element: GrDeleteRepo;
+  let sendStub: sinon.SinonStub;
+
+  setup(async () => {
+    sendStub = sinon.stub();
+    sendStub.returns(Promise.resolve({}));
+
+    element = document.createElement('gr-delete-repo');
+    element.repoName = 'test-repo-name' as RepoName;
+    element.config = {
+      actions: {
+        'delete-project~delete': {
+          label: 'test-action-label',
+          title: 'test-aciton-title',
+          enabled: true,
+          method: HttpMethod.DELETE,
+        },
+      },
+    } as unknown as ConfigInfo;
+    element.plugin = {
+      getPluginName: () => 'delete-project',
+      restApi: () => {
+        return {
+          send: sendStub,
+          invalidateReposCache: () => {},
+        };
+      },
+    } as unknown as PluginApi;
+    document.body.appendChild(element);
+    await element.updateComplete;
+  });
+
+  teardown(() => {
+    document.body.removeChild(element);
+  });
+
+  test('confirm and send', () => {
+    const dialog = queryAndAssert<HTMLElement>(element, '#deleteRepoDialog');
+    dialog.dispatchEvent(new CustomEvent('confirm'));
+    assert.isTrue(sendStub.called);
+    const method = sendStub.firstCall.args[0] as HttpMethod;
+    const endpoint = sendStub.firstCall.args[1] as string;
+    const json = sendStub.firstCall.args[2];
+    assert.equal(method, HttpMethod.DELETE);
+    assert.equal(endpoint, '/projects/test-repo-name/delete-project~delete');
+    assert.deepEqual(json, {force: false, preserve: false});
+  });
+});
diff --git a/web/karma_test.sh b/web/karma_test.sh
new file mode 100755
index 0000000..35430ad
--- /dev/null
+++ b/web/karma_test.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+set -euo pipefail
+./$1 start $2 --single-run \
+  --root 'plugins/delete-project/web/_bazel_ts_out_tests/' \
+  --test-files '*_test.js'
diff --git a/gr-delete-repo/plugin.ts b/web/plugin.ts
similarity index 100%
rename from gr-delete-repo/plugin.ts
rename to web/plugin.ts
diff --git a/web/test/test-setup.ts b/web/test/test-setup.ts
new file mode 100644
index 0000000..ddeaf89
--- /dev/null
+++ b/web/test/test-setup.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import 'chai/chai';
+import '@gerritcodereview/typescript-api/gerrit';
+import {css} from 'lit';
+
+declare global {
+  interface Window {
+    assert: typeof chai.assert;
+    expect: typeof chai.expect;
+    sinon: typeof sinon;
+  }
+  let assert: typeof chai.assert;
+  let expect: typeof chai.expect;
+  let sinon: typeof sinon;
+}
+window.assert = chai.assert;
+window.expect = chai.expect;
+window.sinon = sinon;
+
+window.Gerrit = {
+  install: () => {},
+  styles: {
+    form: css``,
+    menuPage: css``,
+    subPage: css``,
+    table: css``,
+  },
+};
diff --git a/web/test/test-util.ts b/web/test/test-util.ts
new file mode 100644
index 0000000..25a1c79
--- /dev/null
+++ b/web/test/test-util.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export function queryAll<E extends Element = Element>(
+  el: Element,
+  selector: string
+): NodeListOf<E> {
+  if (!el) throw new Error('element not defined');
+  const root = el.shadowRoot ?? el;
+  return root.querySelectorAll<E>(selector);
+}
+
+export function query<E extends Element = Element>(
+  el: Element | undefined,
+  selector: string
+): E | undefined {
+  if (!el) return undefined;
+  const root = el.shadowRoot ?? el;
+  return root.querySelector<E>(selector) ?? undefined;
+}
+
+export function queryAndAssert<E extends Element = Element>(
+  el: Element | undefined,
+  selector: string
+): E {
+  const found = query<E>(el, selector);
+  if (!found) throw new Error(`selector '${selector}' did not match anything'`);
+  return found;
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..d12f85e
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../../tsconfig-plugins-base.json",
+  "compilerOptions": {
+    "outDir": "../../../.ts-out/plugins/delete-project", /* overridden by bazel */
+  },
+  "include": [
+    "**/*.ts"
+  ]
+}