Your application is twenty years old. So what?
I hear the same thing before most audits: "our product runs on Java 1.5, we can't test it automatically."
You can.
An E2E test does not read your code. It opens a browser, clicks, types, and checks what appears. It does not know whether the page was rendered by Java, PHP, .NET WebForms, Django or COBOL behind a web layer. It sees HTML.
That is the one piece of good news in this subject, and it is bigger than it sounds. Your backend language is the variable that matters least.
What actually matters
Six things. None of them has anything to do with your stack.
A reachable environment. A URL that is not production, where the application runs in a known state. Without it, nothing else is possible.
Stable anchors. The test has to find the button again after a redesign. The right answer is dedicated attributes, `data-testid` or equivalent, added once by the developers. The second best is accessible roles and labels: a button named "Confirm order" stays findable when the classes change.
Authentication you can automate. A test account, and preferably a way to log in through an API rather than the form. Signing in through the login screen on every test costs minutes across a full suite.
Control of the data. Two strategies, and you must pick one. Either each test creates what it needs and cleans up after itself, or the environment is reset to a known state before the run. Mixing the two produces exactly the random failures everyone then blames on the tool.
Control of time and randomness. An invoice dated today, a random order number, a calculation based on the clock: if the test cannot freeze those, it will be green one day and red the next.
Isolation. No test should depend on what another one left behind.
That is the list. Six lines, and not one framework named.
The hard cases, and what to do with them
Generated identifiers. Older server frameworks emit ids like `ctl00$ContentPlaceHolder1$gvResults$ctl03$btnEdit`. They change the moment a component moves. Never anchor on them. Target the text, the role, or the position inside a table row identified by its content.
Two frameworks on one page. A React screen next to an Angular 1 screen inside the same product is common in software that has lived. To the browser it is one DOM, so it is one DOM to the test. The real trap is not the mixture, it is that the two halves signal "finished loading" in different ways.
Iframes and third-party windows. Payment, SSO, embedded editors. Both tools can step into them. What costs you is the third party changing its interface without telling you. For payments, test against the provider's sandbox, never the real one.
Server-side rendering into images or canvas. Here there is no DOM to query. A canvas chart, a drawing viewer, a Java-side render turned into an image: the test can check the element appears, has the right size, and that a click at a given coordinate triggers the right action. Going further needs visual comparison, which is a different discipline and a different budget.
The real limits
I would rather say these before signing.
Integrated Windows authentication, NTLM or Kerberos, can be worked around but it costs. Desktop applications that are not web pages are out of scope. And an application with no test environment at all, where the only instance in existence is production, is not a tooling problem. That is an infrastructure problem to solve first.
What this means for you
If your product is fifteen or twenty years old, it keeps the company alive, and nobody dares touch it, the language is not your obstacle.
Your obstacles are the six lines above. They take days to sort out, not months, and most of them are your developers' work rather than ours.
